;(function(factory) {
    "use strict";

    var _cmd_ = {};

    if (typeof exports === 'object') {
        if (typeof require === 'function'){
            var ecmd = require('./ecmd').ECMD;

            _cmd_ = factory;

            Object.keys(ecmd).forEach(function (cmd) {
                    _cmd_[cmd] = ecmd[cmd];
                });

            exports.CMD = _cmd_;
        } else {
            exports.CMD = factory;
        }
    } else {
        _cmd_ = factory;

        if (window && window.mcConst && window.mcConst._CMD_){
            Object.keys(_cmd_).forEach(function (cmd) {
                window.mcConst._CMD_[cmd] = _cmd_[cmd];
            });
        }

        window.getCMD = function getCMD(){
            if (window.hasOwnProperty('_ECMD_')){
                Object.keys(window._ECMD_).forEach(function (cmd) {
                    _cmd_[cmd] = window._ECMD_[cmd];
                });
            }

            return _cmd_;
        }
    }
})({
        Login        : 'login',
        Register     : 'register',
        Reminde      : 'reminde',
        OK           : 'OK',
        Ping         : 'ping',
        History      : 'history',
        NewFoto      : 'newfoto',
        Logs         : 'logs',
        StopLogs     : 'stopLogs',
        Blocked      : 'blocked',
        cLog         : 'cLog',
        AdsFiles     : 'adsFiles',
        checkSSL     : 'checkSSL',
        generateCert : 'generateCert',
        checkPush    : 'checkPush',
        getUrl       : 'getUrl',
        
        getUrlProcess: 'getUrlProcess',
        getUrlAbort  : 'getUrlAbort',
        getUrlError  : 'getUrlError',

        encode_qrcode: 'encode_qrcode',

        // === FTP == browser -> node ================

        bn_ftp_get_list : "bn0001", //
        bn_ftp_login    : "bn0002", //

        // === FTP == node -> browser ================

        nb_ftp_get_list : "nb0001", //
        nb_ftp_login    : "nb0002", //
        nb_ftp_error    : "nb0003", //

        // ===========================================

        sc_drop_connect: '9999',

        // = Client CMD ==============================
        cs_hello                                : '0001', // команда-приветствие, отправляется от клиента при подключении к серверу. С этой команды начинается любое подключение к серверу.
        cs_login                                : '0003', // подключение к серверу под заданной учётной записью. Логин может происходить по одному из вариантов: UIN, ник или адрес электронной почты.
        cs_register_new_user                    : '0004', // зарегистрировать на сервере нового пользователя.
        cs_domain_login                         : '0005', // подключение к серверу с доменной авторизацией в Active Directory.
        cs_restore_account                      : '0006', // восстановление своей учётной записи
        cs_work_time_upload_old_data            : '0007', // отправка на сервер данных о рабочем времени клиента, пока тот был в офлайн (нужна доработка)
        cs_restore_connect_to_channels          : '0008', // восстановить вход в открытые каналы после отключения от сервера (нужна доработка)
        cs_get_bbs                              : "0009", // получить полный текст доски объявлений
        cs_get_adv_new_data                     : "000A", // запрос новых данных на рекламу
        cs_special_ping                         : "000B", // пинг сервера, проверка связи
        cs_ping                                 : "000B", // пинг от клиента, проверка, есть ли соединение. Если длительное время от клиентов не было пинга или любого другого запроса - соединение считается "мёртвым" и автоматически удаляется с сервера.

        cs_ping2                                : "000C",

        cs_get_channels_list                    : "000D", // получить с сервера список созданных каналов
        cs_get_actions_list                     : "000E", // получить список ACTIONs, если они есть и к ним есть доступ
        cs_work_time_control_active_caption     : "000F", // отправка на сервер данных о рабочем времени, нужна доработка
        // deprecated cs_get_small_user_info                  : "0010", // получить краткую информацию о пользователе
        cs_private_request                      : "0011", // запрос на открытие привата с указанным пользователем
        cs_get_hello_msg                        : "0012", // запрос содержимого приветствия канала
        cs_pong                                 : "0013", // ответ на серверную команду sc_ping
        cs_get_contacts_list                    : "0014", // получить список личных персональных контактов.
        cs_get_common_contacts_list             : "0015", // получить список общих контактов (древовидный список контактов, общий для всех пользователей сервера).
        cs_put_msg2txt_channel                  : "0016", // отправить сообщение в текстовый канал
        cs_private_msg                          : "0017", // отправить приватное сообшение другому пользователю
        cs_join_txt_channel                     : "0018", // подключиться к существующему текстовому каналу
        cs_leave_txt_channel                    : "0019", // выйти из текстового канала
        cs_add_new_bbs                          : "001A", // отправить новое сообщение на доску объявлений

        cs_create_txt_channel                   : "001D", // создать новый текстовый канал.
        cs_get_uin_info                         : "001E", // получить полную информацию об указанном пользователе
        cs_get_uin_foto                         : "001F", // получить фотографию пользователя
        cs_get_remote_uin_current_time          : "0020", // получить текущее системное время удалённого пользователя
        cs_send_my_current_time                 : "0021", // отправить свою текущую дату и время указанному пользователю
        cs_get_user_computer_info               : "0022", // получить IP/MAC адрес и название компьютера удалённого клиента
        cs_get_active_window_caption            : "0023", // получить заголовок активного окна пользователя
        cs_active_window_caption                : "0024", // отправка заголовка активного окна
        cs_get_ignores_list                     : "0025", // получить список игноров для указанного пользователя
        cs_update_ignores_list                  : "0026", // обновить персональный список игноров (доработать разбор списка)
        cs_get_favorite_channels                : "0027", // получить список "любимых" каналов
        cs_add_favorite_channel                 : "0028", // добавить канал с указаненым идентификатором в список "любимых"
        cs_delete_favorite_channel              : "0029", // удалить текстовый канал из списка "любимых"
        cs_get_msgtemplates                     : "002A", // получить список персональных "быстрых" сообщений
        cs_update_msgtemplates                  : "002B", // сохранить на сервере в своём аккаунте список "быстрых" сообщений
        cs_set_account_password                 : "002C", // установить новый пароль для своей учётной записи

        cs_add_personal_contact_group           : "001B", // добавить новую группу в персональную панель контактов
        cs_delete_id_from_personal_contacts     : "001C", // удалить группу контактов из личного списка контактов
        cs_add_new_contact                      : "002D", // добавить пользователя в личный список контактов
        cs_rename_contacts_group                : "002F", // переименовать личную группу контактов
        cs_move_personal_contact                : "0030", // перенести персональный контакт из одной группы в другую

        cs_open_remote_computer_shares          : "0031", // окрыть удалённый компьютер по сети Проводником (показать открытые ресурсы (shares))
        cs_forcibly_join_to_text_channel        : "0032", // принудительно включить выбранного пользователя в указанный текстовый канал
        cs_change_txtch_topic                   : "0033", // изменить тему текстового канала
        cs_get_public_ftp_info                  : "0034", // получить информацию с сервера о публичном доступе к общему FTP серверу
        cs_get_channel_history_messages         : "0035", // получить историю сообщений в указанном текстовом канале
        cs_rename_text_channel                  : "0036", // переименовать существующий текстовый канал
        cs_ignore_get                           : "0037", // добавить в игнор пользователя и / или получить информацию об его игнорировании
        cs_add_ignore                           : "0038", // добавить юзера в игнор-список (либо обновить данные)
        cs_clear_text_channel                   : "0039", // очистить выбранный текстовый канал от всех сообщений у всех находящихся в нём пользователей
        cs_personal_message2txtch               : "003A", // персональное/важное сообщение лично другому пользователю в текстовый канал
        cs_block_mac_by_uin                     : "003B", // заблокировать MAC адрес пользователя по его UIN
        cs_block_ip_by_uin                      : "003C", // заблокировать IP адрес пользователя по его UIN
        cs_kill_user                            : "003D", // отключить пользователя от сервера
        cs_kick_from_txt_channel                : "003E", // выгнать пользователя из текстового канала
        cs_private_beep                         : "003F", // отправить пользователю звуковой сигнал
        cs_get_server_uptime                    : "0040", // получить аптайм сервера MyChat
        cs_get_server_version                   : "0041", // получить версию сервера
        cs_halt_user                            : "0042", // отключить пользователя от сервера и завершить его приложение
        cs_set_state                            : "0043", // установить свой сетевой статус (свободен, занят и т.п.)
        cs_typing_notify                        : "0044", // отправить уведомление пользователю о том, что мы ему что-то пишем в привате
        cs_get_user_state                       : "0045", // получить статус другого пользователя
        cs_set_uin_foto                         : "0046", // установить фотографию для своего профиля
        cs_quit                                 : "0047", // пользователь отключается от сервера и посылает ему уведомление об этом
        cs_files_offline_ok                     : "0048", // пользователь согласился принять отложенные для него на сервере файлы
        cs_files_offline_late                   : "0049", // мы не хотим принимать отложенные для нас файлы прямо сейчас, примем их позже
        cs_files_offline_delete                 : "004A", // удалить все файлы, которые были отправлены офлайн указанным пользователем
        cs_get_user_quick_info                  : "004B", // получить краткую информацию об указанном пользователе
        cs_raw_data                             : "004C", // отправка произвольного пакета данных для онлайн-клиента
        cs_raw_data_txt_channel                 : "004D", // отправка произвольного пакета данных в текстовый канал
        cs_action                               : "004E", // выполнение MyChat Action
        cs_stat                                 : '0058', // отправить на сервер статистику использования сервисов чата
        cs_fatal_error_info                     : '0054', // отправить на сервер данные о произошедшей фатальной ошибке, приведшей к краху клиентского приложения

        // === file transfer ===

        cs_files_request_transfer               : '004F', // отправить запрос на отправку файлов и/или папок другому пользователю
        cs_files_transfer_deny                  : '0050', // отказаться принимать файлы от другого пользователя
        cs_files_transfer_accept                : '0051', // согласиться принимать файлы от другого пользователя
        cs_files_transfer_abort                 : '0052', // прервать передачу файлов
        cs_files_internal_send_idx              : '0053', // уведомление о начале передачи очередного файла из списка файлов для отправки
        cs_files_transfer_request_abort         : '0055', // отправить пользователю отказ от принятия файлов
        cs_files_internal_sended_ok_idx         : '0056', // успешно передан файл с указанным индексом из общего списка передаваемых файлов в сеансе связи
        cs_files_transfer_progress              : '0061', // отправить информацию другому пользователю о прогрессе передаваемых файлов. Отправляется не чаще раза в секунду
        cs_files_success_recieved               : '0062', // уведомление отправителю файлов, что все файлы успешно приняты
        cs_files_request_timeout                : '0057', // команда, которая отправляется автоматически, по истечении периода времени, когда получатель ничего не ответил на
                                                          // запрос приёма файлов(например, его не было за компьютером). Передача файлов в этом случае должна быть отменена автоматически
        // ==========================

        cs_send_broadcast                       : '005A', // отправить оповещение
        cs_get_broadcast_presets_names          : '005B', // получить список персональных пресетов для рассылки оповещений
        cs_delete_broadcast_preset              : '005C', // удалить шаблон списка пользователей для оповещения
        cs_add_broadcast_preset                 : '005D', // добавить новый пресет для оповещений
        cs_broadcast_readed_notify              : '005E', // уведомить отправителя оповещения персональным сообщением, что его сообщение только что было прочитано получателем, набор объектов
        cs_broadcast_just_readed                : '005F', // отправить на сервер уведомление об обязательном прочтении полученных оповещений (или одного оповещения), набор объектов

        // ==========================

        cs_get_users_list                       : '0059', // получить с сервера список пользователей (ФИО, пол, аватар и т.п.) согласно запросу
        cs_set_uin_info                         : '0068', // обновить информацию о профиле пользователя
        cs_find_users                           : '0060', // поиск пользователей на сервере согласно заданным критериям. Есть два варианта объекта
        cs_ban_user                             : '0066', // наложить наказание на пользователя (бан). По истении строка наказания оно автоматически снимается сервером.
                                                          // Наказание может наложить только оператор канала
        cs_set_user_online_notify               : '0067', // установить статус онлайн-уведомления о присутствии другого пользователя в сети
        cs_get_private_info                     : '0078', // получить краткую информацию о пользователе, с которым открывается приватный разговор
        cs_send_messages_buffer                 : "0079", // отправка буфера сообщений на сервер перед разрывом соединения

        cs_media_call                           : '007A',
        cs_media_call_accept                    : '007B',
        cs_media_call_reject                    : '007C',
        cs_media_call_close                     : '0080',
        cs_media_call_error                     : '0081', //произошла ошибка при сеансе аудио или видео связи
        cs_media_call_busy                      : '008E',

        cs_web_get_user_foto_file               : '007D',
        cs_web_support_i_am_busy                : '0094', // если пользователю WEB-поддержки с сайта приходит сообщение не от оператора, с кем он в данный момент говорит, он отвечает этой командой

        cs_adm_list_users                       : "0086", // получить список пользователей с сервера по заданным критериям. Нужны административные права
        cs_adm_get_server_info                  : '0090',
        cs_adm_get_user_profile                 : "0091", // получение полной информации о профиле любого пользователя, административная команда
        cs_adm_list_roles                       : '008B', // получить список групп прав пользователей (роли), нужны административные права
        cs_adm_set_user_password                : '0097', // изменить пароль пользователя
        cs_adm_create_user                      : '0096', // создать нового пользователя, административная команда
        cs_adm_remove_user                      : '0095', // удалить пользователя, административная команда
        cs_adm_modify_user_profile              : '0098', // модификация профиля юзера
        cs_adm_clear_user_foto                  : '0099', // удаление фотографии указанного пользователя, админкоманда
        cs_adm_get_server_options               : '009A', // получить все настройки сервера, админкоманда
        cs_adm_get_server_net_interfaces        : '009C', // получить список реальных IP всех сетевых интерфейсов сервера MyChat, админкоманда
        cs_adm_get_user_info_by_preset          : '009D', // получить список любых полей из профиля пользователя, админкоманда
        cs_adm_set_server_options               : '009B', // установить настройки сервера, админкоманда
        cs_adm_remove_all_logs                  : '009E', // удалить все протоколы на сервере, админкоманда
        cs_adm_make_backup_server_db            : '009F', // создать резервную копию базы данных сервера, админкоманда

        cs_adm_set_user_foto                    : "00A0", // назначить фото для пользователя

        cs_adm_get_job_positions_list           : '0093', // получить список должностей компании, административная команда
        cs_adm_job_positions_del                : '00A4', // удалить должность по индексу
        cs_adm_job_positions_add                : '00A3', // добавить новую должность
        cs_adm_job_positions_set_sequence       : '00A2', // установить последовательность должностей компании, массив, админкоманда (последовательсть ID должностей, те, что первые - "старше" тех, кто дальше)
        cs_adm_job_positions_modify             : '00A5', // изменить должность

        cs_adm_role_add                         : '00A6', // добавить новую группу прав
        cs_adm_role_delete                      : '00A7', // удалить группу прав
        cs_adm_role_modify                      : '00A8', // изменить группу прав
        cs_adm_role_set_rights                  : '00A9', // установить новые правила для группы прав
        cs_adm_role_users_list                  : '00AA', // получить список пользователей, входящих в заданную группу прав
        cs_adm_role_apply_to_users              : '00AB', // назначить роль указанному списку пользователей

        cs_send_invite                          : '00AC', // todo: отправить приглашение для любого пользователя присоединиться к MyChat

        cs_adm_check_smtp                       : '00AD', // проверить работоспособность системы отправки почты

        cs_adm_enter_mychat_reg_data            : '00AE', // ввести регистрационные данные для MyChat Server
        cs_adm_enter_mychat_guest_reg_data      : '00AF', // ввести серийный номер для MyChat Guest

        cs_adm_get_websupport_groups            : '00B0', // получить список групп для веб-поддержки
        cs_adm_add_user_to_websupport_group     : '00B1', // перенести пользователя в заданную группу веб-поддержки
        cs_adm_delete_user_from_websupport      : '00B2', // удаление пользователя с группы веб-поддержки
        cs_adm_add_websupport_group             : '00B3', // добавить новую группу WEB-поддержки
        cs_adm_delete_websupport_group          : '00B4', // удалить группу веб-поддержки
        cs_adm_rename_websupport_group          : '00B5', // переименовать группу веб-поддержки
        cs_adm_get_websupport_group_users       : '00B6', // получить список пользователей указанной группы WEB-поддержки

        cs_adm_get_custom_options               : '00B7', // запросить кастомные настройки

        cs_adm_get_total_contacts_list          : '00B8', // запросить общий список контактов
        cs_adm_tcl_add_group                    : '00B9', // добавить новую группу в общий список контактов
        cs_adm_tcl_del_ids_list                 : '00BA', // удалить список групп и/или пользователей из общего списка контактов
        cs_adm_tcl_add_users_list_to_group      : '00E3', // добавить список пользователей в указанную группу
        cs_adm_tcl_move_users_and_groups        : '00E4', // переместить пользователей и/или группы в пределах общего списка контактов
        cs_adm_tcl_rename_group                 : '00E5', // переименовать группу в общем списке контактов

        cs_adm_import_users                     : '00BB', //

        cs_adm_return_server_to_zero_state      : '00BD', // вернуть базу данных MyChat Server в первоначальное состояние, все данные будут удалены. Админкоманда

        cs_adm_set_custom_options               : '00BC', // установить заданные настройки, количество полей может быть переменным
        cs_adm_get_licenses                     : '00BE', // получить информацию по лицензиям MyChat Server

        cs_adm_get_antiflood_filter_data        : '00BF', // получить информацию по антифлуд-фильтрам. Админкоманда
        cs_adm_set_antiflood_filter_data        : '00C0', // установить настройки серверных антифлуд-фильтров. Админкоманда
        cs_adm_modify_flood_except_users        : '00C1', // изменить список пользователей, которых не касаются настройки антифлуд-фильтров
        cs_adm_get_flood_except_users_list      : '00C2', // получить список пользователей, которых не касаются настройки антифлуд-фильтров

        cs_adm_get_allow_ip_list                : '00C3', // получить список IP адресов ("белый" список), которым разрешено подключаться к MyChat Server
        cs_adm_add_new_ip_to_allow_list         : '00C4', // добавить новый IP адрес, маску или диапазон в список разрешённых адресов
        cs_adm_delete_allow_ip                  : '00C5', // удалить IP адрес из списка разрешённых адресов для подключения
        cs_adm_clear_allow_ip_list              : '00C6', // полностью очистить список разрешённых IP адресов

        cs_adm_get_blocked_ip_list              : '00C7', // получить список заблокированных IP адресов (масок и диапазонов). Админкоманда
        cs_adm_add_ip_to_block_list             : '00C8', // добавить IP адрес в список заблокированных
        cs_adm_delete_blocked_ip                : '00C9', // удалить из списка заблокированных IP адрес/маску/диапазон
        cs_adm_clear_blocked_ip_list            : '00CA', // очистить список заблокированных IP адресов/масок/диапазонов

        cs_adm_get_blocked_mac_list             : '00CB', // получить список заблокированных MAC адресов
        cs_adm_delete_blocked_mac               : '00CC', // удалить заблокированный MAC адрес из общего списка
        cs_adm_add_mac_to_block_list            : '00CD', // добавить MAC адрес в список заблокированных
        cs_adm_clear_blocked_mac_list           : '00CE', // очистить список заблокированных MAC адресов

        cs_adm_get_deny_nicks_list              : '00CF', // получить список заблокированных ников в JSON-формате
        cs_adm_add_deny_nick                    : '00D0', // добавить новый ник в заблокированные
        cs_adm_delete_deny_nick                 : '00D1', // удалить ник из списка блокировок
        cs_adm_clear_deny_nicks_list            : '00D2', // полностью очистить список заблокированных ников

        cs_adm_get_deny_conf_list               : '00D3', // получить список заблокированных конференций в JSON-формате
        cs_adm_add_deny_conf                    : '00D4', // добавить новую конференцию в список заблокированных
        cs_adm_delete_deny_conf                 : '00D5', // удалить конференцию из блек-листа
        cs_adm_clear_deny_conf_list             : '00D6', // полностью очистить список заблокированных конференций

        cs_adm_get_bad_words_filters_data       : '00D7', // получить информацию по фильтрам плохих слов в чате
        cs_adm_set_bad_words_filter_data        : '00D8', // сохранить данные по фильтрам "плохих" слов
        cs_adm_get_bad_words_list               : '00D9', // получить полный список "плохих" слов. Админкоманда
        cs_adm_add_bad_word_to_filters          : '00DA', // добавить новое "плохое" слово в список фильтрации, админкоманда
        cs_adm_del_bad_word_from_filters        : '00DB', // удалить "плохое" слово и общего списка
        cs_adm_clear_bad_words_list             : '00DC', // полностью очистить список "плохих" слов, админкоманда
        cs_adm_load_badwords_except_confs       : '00DD', // получить список конференций, в которых отключен фильтр мата
        cs_adm_add_badwords_except_conf         : '00DE', // добавить новую конференцию в список исключений антимат-фильтра
        cs_adm_del_badwords_except_conf         : '00DF', // удалить конференцию из списка исключений антимат-фильтра
        cs_adm_clear_badwords_except_confs      : '00E0', // очистить список исключений конференций антимат-фильтров

        cs_adm_export_users_to_csv              : '00E1', // экспорт списка пользователей в CSV файл
        cs_adm_multiply_modify_users            : '00E2', // множественное изменение данных пользователей
        cs_get_online_users_states              : '00E6', // получить список UIN-ов и статусов всех пользователей, которые в данный момент есть онлайн
        cs_get_depts_list                       : '00E7', // получить список подразделений компании

        cs_adm_get_all_text_confs               : '00E8', // получить список всех текстовых конференций

        cs_adm_force_update_global_contacts     : '00E9', // принудительно обновить кеш общего списка контактов и разослать его всем онлайн-пользователям

        cs_adm_get_bans_list                    : '00EA', // получить список всех банов, которые в данный момент актуальны на сервере. Админкоманда
        cs_adm_del_bans                         : '00EB', // снять указанные баны по индексам, админкоманда
        cs_adm_get_moderators                   : '00EC', // получить список модераторов, админкоманда
        cs_adm_get_conf_names_by_uid_list       : '00ED', // получить список названий конференций по их UID
        cs_adm_delete_moderators                : '00EE', // удалить список модераторов
        cs_adm_add_moderator                    : '00EF', // добавить модератора конференции
        cs_adm_del_moderator_confs              : '00F0', //

        cs_get_broadcasts_history               : '00F1', //

        cs_get_random_uin                       : '00F3', // получить случайный UIN из списка людей онлайн
        cs_set_custom_ignore                    : '00F4', // добавить игнор для указанного пользователя
        cs_del_custom_ignore                    : '00F5', //  игнор для указанного пользователя

        cs_adm_get_auto_conf_list               : '00F6', // получить список автосоздаваемых конференций на сервере
        cs_adm_del_auto_conf                    : '00F7', // удаление автосоздаваемой конференции по идентификатору
        cs_adm_add_auto_conf                    : '00F8', // создать новый автосоздаваемый канал
        cs_adm_update_auto_conf                 : '00F9', // обновить данные об австосоздаваемой конференции
        cs_adm_add_group_to_auto_conf           : '00FA', // добавить группу для включения в автосоздаваемую конференцию
        cs_adm_del_group_from_auto_conf         : '00FB', // удалить группу пользователей из автосоздаваемой конференции

        cs_adm_get_bbs_msg_list                 : '00FC', // получить список актуальных сообщений на доске объявлений
        cs_adm_bbs_clear_actual                 : '00FD', // удалить все актуальные сообщения на доске объявлений
        cs_adm_bbs_get_msg                      : '00FE', // получить данные сообщение на доске объявлений по его идентификатору
        cs_adm_bbs_modify_msg                   : '00FF', // изменить сообщение на доске объявлений, админкоманда
        cs_adm_bbs_del_messages                 : '0100', // удалить актуальные сообщения на доске объявлений по их индексу
        cs_adm_add_bbs_message                  : '0101', // добавить новое сообщение на доску объявлений

        cs_adm_save_ad_options                  : '0102', // сохранить данные по настройке Active Directory интеграции
        cs_adm_get_ad_options                   : '0103', // получить настройки по Active Directory интеграции

        cs_adm_import_ad_users                  : '0104', // импортировать выбранных пользователей active directory

        cs_adm_get_update_packet_data           : '0105', // получить данные update-пакета для клиентов MyChat
        cs_adm_upload_update_packet             : '0106', // загрузить пакет обновления на сервер. Административная команда

        cs_adm_get_online_users_list            : '0107', // получить список онлайн-пользователей
        cs_adm_kill_users                       : '0108', // отключить от сервера указанный список пользователей (разорвать соединение). В ответ приходит sc_adm_command_complete с командой cs_adm_kill_users
        cs_adm_halt_users                       : '0109', // отправить списку пользователей команду завершения работы приложения с последующим отключением от сервера.
                                                          // В ответ приходит sc_adm_command_complete с командой cs_adm_halt_users
        cs_adm_get_ad_users_list                : '010A', // получить с сервера Active Directory список пользователей
        cs_sync_private_history_last            : '010B', // получить последние N сообщений из приватного разговора с указанным пользователей. Можно запросить максимум 100 сообщений
        cs_sync_private_history                 : '010C', // получить логи разговоров в привате (синхронизация)

        cs_adm_get_script                       : '010D', // получить скрипт по его ID
        cs_adm_get_script_event_states          : '010E', // получить список статусов доступных скриптов. Нужны админправа
        cs_adm_script_check_syntax              : '010F', // проверить синтаксис написанного серверного скрипта
        cs_adm_script_onoff                     : '0110', // включение выключение скрипта
        cs_adm_script_run                       : '0111', // запуск скрипта
        cs_adm_script_delete                    : '0112', // удалить скрипт
        cs_adm_script_save                      : '0113', // сохранить скрипт
        cs_adm_script_create                    : '0114', // создать скрипт

        cs_adm_restart_server_services          : '0115', // принудительный перезапуск встроенного WEB-сервера, админкоманда

        cs_adm_get_active_conf_list             : '011B', // получить список работающих конференций, админкоманда
        cs_adm_kill_txt_conf                    : '011C', // удалить текстовую конференцию с сервера (не автосоздаваемую). Админкоманда. В ответ приходит complete: cs_adm_kill_txt_conf либо ошибка
        cs_adm_get_users_in_conf                : '011D', // получить список пользователей в конференции
        cs_adm_kick_users_from_conf             : '011E', //выгнать из конференции указанных пользователей

        cs_info                                 : '077C', //

        cs_adm_remove_conversations             : '0183', // удалить любые протоколы разговоров на сервере и у клиентов за всё время
        cs_live_reconnect                       : '01AF', // лайв переподключение андроида

        // ==== KANBAN =========================================================

        cs_kanban_get_projects_list             : '0117', // получить список проектов kanban
        cs_kanban_get_stages_list               : '0118', // получить список этапов проекта
        cs_kanban_get_tasks_list                : '0119', // получить список задач kanban-доски
        cs_kanban_get_tags_list                 : '011A', // получить список тегов kanban-доски
        cs_kanban_add_project                   : '0120', // добавить новый проект на kanban-доску
        cs_kanban_add_stage                     : '0121', // добавить новый stage на kanban доску
        cs_kanban_delete_stage                  : '0122', // удалить этап указанного проекта с kanban-доски
        cs_kanban_modify_stage                  : '0123', // изменить этап проекта kanban-доски
        cs_kanban_add_task                      : '013B', // добавить новую задачу на kanban-доску
        cs_kanban_task_move                     : '013C', // перенос задачи в новый stage
        cs_kanban_task_remove                   : '013D', // удаление задачи
        cs_kanban_task_edit                     : '013E', // редактирование задачи
        cs_kanban_add_comment                   : '013F', // добавление нового комментария к таску на kanban-доске
        cs_kanban_get_comments                  : '0140', // получить список комментариев к задаче на kanban-доске
        cs_kanban_delete_comment                : '0141', // удалить комментарий к задаче с доски kanban
        cs_kanban_close_project                 : '0142', // закрыть проект kanban
        cs_kanban_reopen_project                : '0143', // открыть заново уже закрытый проект kanban
        cs_kanban_delete_project                : '0144', // удалить kanban проект
        cs_kanban_edit_project                  : '0145', // изменить kanban-проект
        cs_kanban_get_personal_options          : '0149', // получить персональный набор опций клиента по его UIN
        cs_kanban_save_personal_options         : '014A', // сохранить персональный набор настроек. JSON объект, любые поля

        cs_kanban_get_project_info              : '014C', // получить краткую информацию о проекте
        cs_kanban_get_task_info                 : '014D', // получить краткую информацию о задаче

        cs_kanban_get_last_performers           : '0153', // получить последних исполнителей задач для указанного проекта
        cs_kanban_get_project_tags_list         : '0168', // получить список тегов для конкретного проекта
        cs_kanban_get_dashboard                 : '0169', // получить краткую сводную информацию о проектах Kanban

        // ==============================================================

        cs_get_personal_options                 : '0149', // получить персональный набор опций клиента по его UIN
        cs_save_personal_options                : '014A', // сохранить персональный набор настроек. JSON объект, любые поля

        cs_browser_info                         : '0124', // информация о браузере пользователя, если это web-клиент

        cs_media_offer                          : '0125', // создание медиа-запроса к клиенту
        cs_media_answer                         : '0126', // создание медиа-ответа на запрос media offer
        cs_media_ice_candidate                  : '0127', // ответ STUN/TURN сервера для собеседника
        cs_media_change_settings                : '0128', // изменение настроек медиа-данных пользователя (аудио/видео)
        cs_media_ready                          : '0129', // NodeWebKit клиента успешно стартовал и работает
        cs_media_exit_from_mid                  : '012A', // пользователь вышел из media-канала

        cs_import_personal_contacts             : '012B', // импорт персональных контактов

        cs_adm_get_client_options_presets       : '012C', // получить список шаблонов настроек клиентов, админкоманда
        cs_adm_del_client_options_preset        : '012D', // удалить шаблон настроек клиента по его идентификатору, админкоманда
        cs_adm_create_client_options_preset     : '012E', // создать новый шаблон настроек для клиентов, админкоманда
        cs_adm_update_client_options_preset     : '012F', // изменить шаблон настроек, админкоманда
        cs_adm_apply_roles_to_options_presets   : '0131', // назначить роли групп прав пользователей для наборов настроек, массив

        cs_adm_add_new_user_group               : '0132', // создать новую группу пользователей
        cs_adm_get_user_groups                  : '0133', // получить список существующих групп пользователей
        cs_adm_get_group_users_list             : '0134', // получить список пользователей указанный группы
        cs_adm_add_users_to_group               : '0135', // добавить список пользователей в группу
        cs_adm_remove_users_from_group          : '0136', // удалить пользователей из группы
        cs_adm_rename_group_of_users            : '0137', // переименовать указанную группу пользователей
        cs_adm_delete_groups_list               : '0138', // удалить список групп пользователей

        cs_adm_set_integration_tool_options     : '0139', // установить настройки интеграции инструмента (phpbb... etc.)
        cs_adm_get_integration_tool_options     : '013A', // получить настройки интеграции инструмента (phpbb... etc.)

        cs_adm_get_check_domains_list           : '0146', // получить список доменов с адресами и портами, которые используются для проверки наличия пользователей в данных доменах
        cs_adm_add_check_domain                 : '0147', // добавить домен в список доменов для проверки возможности логинов доменных пользователей
        cs_adm_delete_check_domain              : '0148', // удалить домен из списка доменов для проверки подключающихся доменных пользователей

        cs_adm_get_allowed_and_blocked_plugins  : '014E', // получить списки разрешённых и запрещённых плагинов
        cs_adm_add_allowed_or_blocked_plugins   : '014F', // добавить названия плагинов в список запрещённых или разрешённых клиентских плагинов
        cs_adm_delete_allowed_or_blocked_plugin : '0150', // удалить плагин из списка разрешённых или запрещённых клиентских плагинов

        cs_adm_change_mychat_server             : '0151', // изменить параметры подключения клиентов к серверу MyChat
        cs_adm_get_script_info                  : '0152', // получить краткую информацию о скрипте, админкоманда

        // == FORUM ===========================================

        cs_forum_get_structure                  : '0156', // получить структуру форума (разделы и топики, в JSON)
        cs_forum_create_section                 : '0157', // добавить новый раздел в форум
        cs_forum_delete_section                 : '0158', // удаление раздела форума
        cs_forum_change_section                 : '0159', // перемещение/переименование раздела форума
        cs_forum_create_topic                   : '015A', // добавить новый топик в форум
        cs_forum_change_topic                   : '015B', // перемещение/переименование топика форума
        cs_forum_create_post                    : '015C', // добавить новый пост в форум

        cs_forum_delete_post                    : '015D', // удалить сообщение из форума
        cs_forum_delete_topic                   : '015E', // удалить тему из форума
        cs_forum_change_post                    : '015F', // перемещение/переименование сообщения форума
        cs_forum_get_topic_posts                : '0160', // получить список сообщений указанного топика форума
        cs_forum_get_section_info               : '0161', // получить информацию о секции (список топиков, краткая информация о них: LastChange, UIN, DisplayName, Sex, Avatar, DateTime, CountMessages)

        cs_get_book                             : '0164', // получить справочник с сервера
        cs_is_file_exists                       : '0165', // узнать, есть ли файл с таким хэш-кодом на сервере. Параметры Where и ID потом вернутся обратно пользователю в команде-ответке
        cs_get_image_thumbs                     : '0166', // получить уменьшенную копию изображения с сервера

        cs_create_token                         : '0154', // создать токен для быстрого доступа в какой-нибудь сервис чата
        cs_login_by_token                       : '0155', // логин в MyChat/kanban/forum/web по токену
        cs_get_removed_user_info                : '016F', // получить краткую информацию об удалённом пользователе

        cs_adm_get_integration_telegram_users   : '016B', // получить список пользователей для интеграции с Telegram
        cs_adm_integration_telegram_delete_user : '016C', // удалить Telegram пользователя из списка для интеграции
        cs_adm_integration_telegram_modify_user : '016D', // изменить параметры Telegram пользователя из списка для интеграции
        cs_adm_integration_telegram_clear_users_list: '016E', // очистить список пользователей для интеграции с Telegram

        cs_integration_icq_set_account          : '0170', // привязать ICQ аккаунт к своей учётной записи
        cs_integration_icq_send_message         : '0171', // отправить сообщение пользователю ICQ
        cs_adm_delete_conf_message              : '0172', // удалить сообщение из текстовой конференции

        cs_adm_get_history_info_private         : '0173', // админкоманда, получить информацию по сообщениям в привате за диапазон времени. В ответ приходит 8107 sc_adm_history_info_private
        cs_adm_get_history_private              : '0174', // админкоманда, получение истории приватов двух юзеров по диапазону индексов
        cs_adm_get_history_info_conf            : '0175', // админкоманда, получить информацию по сообщениям в конференции за диапазон времени. В ответ приходит 8109 sc_adm_history_info_conf
        cs_adm_get_history_conf                 : '0176', // админкоманда, получение истории в конференции по диапазону индексов
        cs_adm_get_history_private_user_pairs   : '0177', // админкоманда, получить все пары приватных разговоров за все время, ответ будет 810B sc_adm_history_private_user_pairs

        cs_adm_check_ports                      : '017A', // проверить, не заняты ли порты указанных сервисов, админкоманда. Если всё ок, будет complete, если нет - ошибка с первым "неудачным" портом

        cs_moder_delete_conf_message            : '017B', // удалить сообщение из текстовой конференции, нужны права для удаления сообщений в конференциях из раздела "Администрирование"

        cs_adm_reconnect_users2autoconf         : '017D', // добавить/выгнать пользователей автосоздаваемой конференции, которую только что меняли

        cs_adm_get_blocked_hwid_list            : '017E', // получить список заблокированных HardwareID, админкоманда
        cs_adm_delete_blocked_hwid              : '017F', // удалить заблокированный HardwareID адрес из общего списка
        cs_adm_clear_blocked_hwid_list          : '0180', // очистить список заблокированных HardwareID на сервере
        cs_adm_add_hwid_to_block_list           : '0181', // добавить HardwareID клиента в список блокировок
        cs_adm_get_system_logs                  : '0182', // получить системные логи за период времени, админкоманда

        cs_get_options_preset                   : '0130', // получить шаблон настроек по его идентификатору

        cs_adm_modify_check_domain              : '0185', // отредактировать настройки для указанного домена
        cs_adm_get_domains_list                 : '0186', // получить список доменов, с которыми может работать сервер MyChat
        
        cs_adm_forum_get_subs_groups            : '0187', // получить список групп пользователей, которые подписаны на конкретную тему либо секцию
        cs_adm_forum_add_subs_group             : '0188', // подписать группу пользователей на тему или секцию форума
        cs_adm_forum_delete_subs_group          : '0189', // удалить группу пользователей из подписки на секцию или тему форума
        cs_adm_forum_clear_subs                 : '018A', // удалить все подписки на тему или раздел форума (группы и пользователи)
        cs_adm_forum_add_subs_user              : '018B', // подписать пользователя на тему или секцию форума
        cs_adm_forum_delete_subs_user           : '018C', // удалить пользователей из подписки на секцию или тему форума
        cs_adm_forum_get_subs_users             : '018D', // получить список пользователей, которые подписаны на конкретную тему либо секцию

        cs_sync_conf_history                    : '0179', // получить логи разговоров в конференции (синхронизация). Пользователь должен находиться в указанной конференции
        cs_get_forum_subs                       : '018E', // получить список подписок на темы и разделы форума
        cs_forum_toggle_subs                    : '018F', // добавить или убрать подписку на тему либо раздел (секцию) форума
        cs_set_custom_options                   : '0190', // сохранить кастомные настройки клиента на сервере
        cs_get_custom_options                   : '0191', // получить кастомные настройки клиента на сервере

        cs_adm_integration_quiz_check_question  : '0193', // проверить существование вопроса в базе робота-викторины по тексту вопроса или по номеру
        cs_adm_integration_quiz_add_question    : '0194', // добавить новый вопрос в базу робота-викторины
        cs_adm_integration_quiz_del_question    : '0195', // удалить вопрос из базы робота-викторины по номеру
        cs_adm_integration_quiz_modify_question : '0196', // редактировать вопрос в базе робота-викторины по индексу
        cs_adm_integration_quiz_get_stat        : '0197', // получить статистику робота-викторины
        cs_adm_integration_quiz_clear_stat      : '0198', // очистить статистику робота-викторины
        cs_adm_enter_quiz_reg_data              : '0199', // ввести серийный номер для робота-викторины

        cs_users_live_search                    : '019A', // найти список пользователей на сервере
        cs_get_private_dialogs                  : '019B', // получить список своих приватных диалогов
        cs_del_private_dialog                   : '019C', // удалить диалог с указанным юзером из списка приватных диалогов
        cs_private_msg_read                     : '019D', // пользователь только что прочитал приватное сообщение из указанного диалога
        cs_private_msg_got                      : '019E', // пользователь только что получил приватное сообщение из указанного диалога
        cs_private_get_dialog_msg_states        : '019F', // получить информацию о полученных и прочитанных сообщениях в приватном диалоге с оппонентом

        cs_adm_delete_private_message           : '01A0', // удалить сообщение из приватного диалога

        cs_delete_private_message               : '01A1', // удалить сообщение из привата, нужны права для удаления произвольных сообщений в своих приватах
        cs_redirect_private_talk                : '01A2', // перенаправить приватный разговор от одного пользователя к другому

        cs_get_users_for_redirect_dialog        : '01A3', // получить список пользователей, на которых можно сделать перенаправление диалога
        cs_forward_messages                     : '01A4', // (7.4+) переслать сообщения из привата или конференции
        cs_private_multicast_msg                : '01A5', // (7.4+) отправить приватное сообщение нескольким пользователям
        cs_create_presenter                     : '01A6', // (7.4+) создать и сохранить presenter страницы для форума
        cs_adm_terminal_command                 : '01A7', // выполнить команду из консоли админки

        cs_create_media_conf                    : '01A8', // создать медиа-конференцию или вебинар

        cs_get_delayed_commands                 : '01A9', // получить список отложенных команд

        cs_adm_modify_allow_ip                  : '01AA', // изменить IP, маску или диапазон в фильтре разрешённых IP адресов
        cs_adm_modify_blocked_ip                : '01AB', // изменить IP, маску или диапазон в фильтре запрещённых IP адресов

        cs_kanban_task_take                     : '01AC', // взять задачу в работу

        cs_get_users_changeid_data              : '01AD', // получить краткую информацию о списке пользователей
        cs_get_user_avatars                     : '01AE', // получить аватары пользователей (прямоугольные картинки)

        cs_conf_got_message                     : '01B0', // юзер получил сообщение в конференции
        cs_conf_read_message                    : '01B1', // юзер прочитал сообщение в конференции

        cs_media_busy                           : '01B2', // передать сигнал "занято" для входящего звонка, сервер обрабатывает эту команду и генерирует приватное сообщение от имени звонящего с типом сообщения MSG_TYPE_MEDIA_CALL (12) и Type=mediastate_call_busy (10)

        cs_halt_cid                             : '01B4', // отключить CID пользователя от сервера и завершить его приложение
        cs_kill_cid                             : '01B5', // отключить CID пользователя от сервера

        cs_adm_asterisk_do_connect              : '01BB', // подключиться к серверу Asterisk
        cs_adm_asterisk_send_command            : '01BC', // выполнить команду Asterisk
        cs_adm_dismiss_an_employee              : '01BD', // уволить одного или несколько сотрудников, пользователь блокируется, удаляется из всех списков контактов и из групп пользователей

        cs_adm_integration_turbosms_check_balance: '01BF', // получить баланс состояния счёта в сервисе TurboSMS
        cs_adm_integration_turbosms_send_message : '01C0', // отправить SMS либо Viber-сообщение на указанный номер телефона

        cs_adm_modify_common_user_group         : '01C3', // изменить группу пользователей
        cs_adm_create_personal_user_group       : '01C4', // создать новую личную группу пользователей
        cs_adm_get_personal_user_groups         : '01C5', // получить список персональных групп указанного пользователя
        cs_adm_modify_personal_user_group       : '01CA', // изменить персональную группу пользователей
        cs_adm_add_users_to_private_group       : '01CB', // добавить пользователей в личную группу указанного пользователя
        cs_adm_remove_users_from_private_group  : '01CC', // удалить пользователей из личной группы пользователя
        cs_adm_get_personal_group_users_list    : '01CD', // получить список пользователей персональной группы

        cs_kanban_add_user_group_to_project     : '01C6', // дать доступ группе пользователей к канбан-проекту
        cs_kanban_add_access_user_groups_to_stage: '01C8', // дать доступ списку групп пользователей к этапу канбан-проекта, ответка -
        cs_create_personal_user_group           : '01CE', // создать новую личную группу пользователей
        cs_remove_personal_user_group           : '01CF', // удалить личные группы пользователя
        cs_add_users_to_personal_user_group     : '01D0', // добавить список пользователей в личную группу пользователя
        cs_get_personal_user_group_data         : '01D1', // получить данные персональной группы пользователей
        cs_remove_users_from_personal_user_group: '01D2', // удалить пользователей из личной группы пользователя
        cs_get_personal_user_groups             : '01D3', // получить список персональных групп пользователя
        cs_rename_personal_user_group           : '01D4', // переименовать персональную группу пользователей
        cs_get_public_user_groups               : '01D5', // получить список публичных групп пользователей
        cs_get_kanban_rights_group_list         : '01D6', // получить список групп прав с доступом к канбану
        cs_get_kanban_get_projects_groups       : '01D7', // получить список групп проекта для разграничения прав доступа
        cs_kanban_remove_user_groups_from_project: '01C7', // убрать доступ для групп пользователей в канбан-проекте
        cs_get_common_user_group_data           : '01D8', // получить данные общей группы пользователей

        cs_adm_get_all_user_group_membership_list: '01D9', // получить список общих и личных групп, куда входит указанный пользователь, ответ - sc_all_user_group_membership_list

        cs_geo_position                         : '01DA', // текущая геопозиция устройства пользователя ответ на запрос 815C sc_get_geo_position (8.12+)
        cs_get_user_gps_history                 : '01DB', // запрос истории перемещений пользователя за период времени
        cs_get_user_gps_position                : '01DC', // отправить запрос на текущие GPS координаты всем онлайн-приложениям указанного пользователя

        cs_adm_fire_users                       : '01DD', // уволить одного или нескольких сотрудников, учётные записи блокируются, пользователи удаляются из общего и личных списков контактов и из всех групп ответ на команду - ошибка или sc_users_fired

        cs_media_conf                           : '01DE', // работа с медиаконференциями, универсальная команда

        cs_adm_custom_menu_create               : '01DF', // создать custom-меню для клиентских приложений
        cs_adm_get_custom_menu_list             : '01E0', // получить список существующих custom-меню для клиентских приложений
        cs_adm_remove_custom_menu_list          : '01E1', // удалить список шаблонов кастомных меню по их индексам, если всё ок, то отправляется команда sc_adm_command_complete
                                                          // если есть ошибки - то 8001 sc_error с указанием номера, что не так
        cs_adm_modify_custom_menu               : '01E2', // изменить шаблон кастомного меню по индексу
        cs_adm_add_group_to_custom_menu         : '01E3', // добавить группу пользователей в custom-menu
        cs_remove_groups_from_custom_menu       : '01E4', // удалить группы пользователей из custom-menu, если всё ок, то ответка - sc_adm_command_complete
        cs_get_custom_menu                      : '01E5', // получить данные своего custom-menu, ответ, если меню есть - 8164 sc_custom_menu (8.14+)
                                                          // если меню нету, никакого ответа не будет, ошибки тоже не будет
        cs_remote_exec                          : '01E6', // удалённый запуск скрипта на сервере, скриптовое событие OnClientRemoteExec. Нужны права

        cs_media_conf_leave                     : '01E8', // пользователь выходит из медиаконференции
        cs_media_conf_join                      : '01E9', // пользователь подключается к уже созданной медиаконференции

        cs_adm_ad_get_user_photo_thumbs         : '01EB', //  получить миниатюры фотографий пользователей из домена
        cs_adm_ad_sync_now                      : '01ED', // административная команда для немедленного выполнения синхронизации с доменом

        cs_adm_check_user_pwd                   : '01EF', // проверка, правильный ли пароль у пользователя, ответ на запрос - sc_adm_check_user_pwd_state

        cs_media_conf_close                     : '01F0', // пользователь закрывает медиаконференцию, все автоматически выходят из неё
        cs_media_conf_broadcast                 : '01F2', // универсальная команда рассылки сообщения всем пользовтелям медиа конференции, кроме отправителя

        cs_get_fresh_users_data                 : '01F3', // получить свежую информацию по изменениям ChangeID пользователей сервера, всех, у кого этот показатель больше
                                                          // указанного числа
        cs_login_done                           : '01F4', // клиент сигнализирует, что он получил от сервера основную порцию данных для первичного подключения и готов принимать оставшиеся данные
        cs_get_conf_user_list                   : '01F5', // получить список пользователей указанной конференции. Пользователь должен в ней находиться
                                                          // команда отправляется, если на клиент-сайде нет списка юзеров конференции или не совпадает
                                                          // контрольная сумма "UserListCRC32" из команды sc_join_txt_channel. Ответка -
      
        cs_close_text_conf                      : '01F6', // закрыть текстовую конференцию. Для этого должны быть права на закрытие конференций либо юзер должен быть owner-ом указанной конфы.
                                                          // Нельзя закрыть автосоздаваемую конференцию, у которой owner Elisa (UIN 0). Если в данный момент в конфе происходит звонок - он будет прекращён.
                                                          // Необязательно находиться в конференции. Если конфа будет успешно закрыта, то она блокируется, убирается из онлайн-структур сервера,
                                                          // а все юзеры получают уведомление про выход из этой конференции

        cs_adm_change_text_conf_owner           : '01F7', // сменить владельца текстовой конференции. Если владелец uin=0, то функция вернёт ошибку.
        cs_adm_get_user_conf_list               : '01F8', // получить список конференций, куда входит указанный пользователь, ответ - sc_adm_user_conf_list

        cs_adm_upload_badword_list              : '01FA', // загрузить на сервер список "плохих" слов для фильтра, список дополнит сущетвующий и в ответ придёт команда sc_badword_list_uploaded
                                                          // с информацией, сколько слов добавлено, либо ошибка
        cs_try_to_change_private_message        : '01B8', // попытка отредактировать своё сообщение в привате.
                                                          // редактировать можно только свои последние сообщения в течение заданного периода времени
        cs_try_to_change_conf_message           : '01B9', // попытка отредактировать своё сообщение в конференции, отправленное только в течение указанного в настройках сервера периода времени


        // =========================================

        adm_ldap_get_user_list: 'ldap_get_user_list',
        adm_ldap_get_user_info: 'ldap_get_user_info',

        adm_ldap_user_list: 'ldap_user_list',
        adm_ldap_user_info: 'ldap_user_info',

        // Server CMD ==============================

        sc_hello                                : '8000', // ответ на соединение клиента (сигнатура версии которую присылает клиент, например "mc5.0")
        sc_error                                : "8001", // произошла какая-то ошибка
        sc_accept_connection                    : "8002", // сервер разрешает соединение
        sc_free_edition                         : "8003", // сообщение клиенту, что тот подключен к бесплатному серверу MyChat
        sc_get_all_rights                       : "8004", // список прав клиента на сервисы чата, а также список разрешённых и запрещённых плагинов на данном сервере
        sc_login                                : "8005", // fucking login...
        //sc_job_positions                        : "8006", // * список должностей компании. Нужна доработка
        sc_remote_set_options_file              : "8007", // удалённая отправка списка настроек клиенту (файл). Будет удалено, останется только команда 8022 sc_remote_set_options
        sc_get_channels_list                    : "8008", // список каналов, которые созданы на сервере
        sc_bbs_changed                          : "8009", // уведомление клиента о том, что была изменена доска объявлений
        sc_offline_private                      : "800A", // офлайн приватное сообщение для клиента
        sc_private                              : "800B", // обычное приватное сообщение для клиента
        // !!! deprecated sc_get_small_user_info                  : "800C", // получить краткую информацию о пользователе (доделать)
        sc_show_advert_block                    : "800D", // показать рекламный блок в клиенте
        sc_hide_advert_block                    : "800E", // спрятать рекламный блок
        sc_pong                                 : "800F", // отправка с сервера команды-реакции на cs_ping, проверка связи
        sc_actions_list                         : "8010", // пришёл список команд ACTIONs с сервера, нужна доработка
        sc_files_offline_exists                 : "8011", // есть отложенные файлы для клиента
        sc_grant_private_access                 : "8012", // разрешение общаться с пользователем в привате, краткая информация о собеседнике
        sc_join_txt_channel                     : "8013", // вход пользователя в текстовый канал
        sc_txt_channel_hello_msg                : "8014", // текст приветствия указанного текстового канала
        sc_txt_channel_give_grant               : "8015", // получение прав на текстовый канал
        sc_ulist_in_txt_ch                      : "8016", // список пользователей в текстовом канале
        // !!! deprecated sc_join_newbie_txt_channel              : "8017", // новый пользователь вошёл в текстовый канал
        // !!! deprecated sc_join_newbies_txt_channel             : "8073", // новые пользователи вошли в текстовый канал
        sc_online_notify                        : "8018", // уведомление о том, что пользователь, который нам интересен, появился в онлайн
        sc_broadcast                            : "8019", // текстовое оповещение
        sc_user_online                          : "801A", // указанный пользователь подключен к серверу
        sc_trial                                : "801B", // информация о том, что используемая версия сервера незарегистрирована, и сколько дней осталось до конца работы trial-версии
        sc_remote_set_options                   : "801C", // настроить клиент в соответствии с пришедшим набором настроек в JSON формате. Доделать описание
        sc_get_msgtemplates                     : "801D", // с сервера пришёл список шаблонов для "быстрых" сообщений. Нужна переделка
        sc_get_contacts_list                    : "801E", // список личных контактов клиента. Доделать
        sc_get_common_contacts_list             : "801F", // список общих контактов клиента. Доделать
        sc_ping                                 : "8020", // 8020 принудительный пинг с сервера, проверка связи при плохом соединении, нужно при получении ответить командой cs_pong
        sc_ignore                               : "8021", // отправленное сообщение было проигнорировано другим пользователем
        sc_user_state                           : "8022", // текущий статус пользователя
        sc_leave_txt_channel                    : "8023", // пользователь вышел из указанного канала, информационное уведомление
        sc_kill_txt_ch                          : "8024", // текстовый канал удалён с сервера
        sc_bbs_body                             : "8025", // содержимое доски объявлений, доработать
        sc_add_personal_contact_group           : "8026", // в персональную панель контактов была успешно добавлена новая группа
        sc_delete_personal_contact_group        : "8027", // удалить группу из личного списка контактов
        sc_txt_channel_created                  : "8028", // уведомление о том, что на сервере создан новый текстовый канал
        sc_get_uin_info                         : "8029", // получение полной информации о пользователе
        sc_get_remote_uin_current_time          : "802A", // получить текущее системное время удалённого пользователя (remote user)
        sc_user_current_time                    : "802B", // текущие системные дата и время указанного пользователя
        sc_user_foto                            : "802C", // информация о фотографии указанного пользователя
        sc_user_computer_info                   : "802D", // информация об IP/MAC адресе пользователя и сетевом имени его компьютера
        sc_get_active_window_caption            : "802E", // запрос сервера у клиента заголовока активного окна
        sc_user_active_window_caption           : "802F", // ответ сервера клиенту с заголовока активного окна
        sc_ignores_list                         : "8030", // список игноров для указанного пользователя // доработать разбор строки
        sc_favorite_channels_list               : "8031", // список "любимых" каналов пользователя
        sc_change_password                      : "8032", // для пользователя установлен новый пароль к его учётной записи. Команда приходит в ответ на попытку изменения собственного пароля, либо при изменении пароля пользователя принудительно, на сервере, администратором
        sc_add_new_contact                      : "8033", // добавить новый контакт в персональный список контактов
        sc_delete_contact                       : "8034", // удалён контакт из личного списка контактов
        sc_rename_contacts_group                : "8035", // переименована личная группа контактов
        sc_move_personal_contact                : "8036", // персональный контакт перенесён в новую группу
        sc_open_remote_computer_shares          : "8037", // открыть удалённый компьютер по сети Проводником (показать открытые ресурсы (shares))
        sc_txtch_topic_changed                  : "8038", // изменена тема текстового канала
        sc_public_ftp_info                      : "8039", // информация о публичном доступе к FTP серверу MyChat
        sc_channel_history_messages             : "803A", // получена история сообщений в текстовом канале за временной период
        sc_rename_text_channel                  : "803B", // текстовый канал переименован
        sc_user_ignore_info                     : "803C", // информация об игнорировании указанного пользователя
        sc_clear_text_channel                   : "803D", // очистить выбранный текстовый канал от всех сообщений
        sc_personal_message2txtch               : "803E", // персональное/важное сообщение лично другому пользователю в текстовый канал
        sc_kick_from_txt_channel                : "803F", // пользователя выгнали из текстового канала
        sc_private_beep                         : "8040", // отправить пользователю звуковой сигнал
        sc_server_uptime                        : "8041", // сколько времени работает сервер MyChat без перезагрузки
        sc_server_version                       : "8042", // информация о версии сервера
        sc_user_offline                         : "8043", // пользователь с указанным UIN о тключился от сервера
        sc_halt                                 : "8044", // немедленно отключиться от сервера и завершить работу приложения в принудительном порядке
        sc_typing_notify                        : "8045", // уведомление о том, что нам в привате в данный момент кто-то пишет ответ
        // !!! deprecated sc_update_user_data                     : "8046", // обновить информацию об указанном пользователей
        sc_change_server                        : "8047", // изменить параметры подключения к серверу MyChat
        sc_user_quick_info                      : "8048", // краткая информация о пользователе
        sc_raw_data                             : "8049", // отправка произвольного пакета данных для онлайн-клиента
        sc_raw_data_txt_channel                 : "804A", // отправка произвольного пакета данных в текстовый канал

        sc_files_offline_send_accept            : "804B", // подтверждение на отправку файлов в офлайн (на сервер) для указанного пользователя
        sc_files_request_transfer               : "804C", // запрос на приём файлов от указанного пользователя
        sc_files_transfer_deny                  : "804D", // пользователь отказался принимать файлы
        sc_files_transfer_accept                : "804E", // пользователь согласился принимать файлы
        sc_files_transfer_abort                 : "804F", // пользователь прервал передачу файлов
        sc_file_internal_send_idx               : "8050", // идёт передача очередного файла
        sc_files_transfer_request_abort         : "8051", // пользователь ответил отказом на запрос приёма файлов
        sc_file_internal_sended_ok_idx          : "8052", // очередной файл успешно передан
        sc_files_request_timeout                : "8053", // отказ от приёма файлов по тайм-ауту
        sc_files_transfer_progress              : '8059', // информация о прогрессе передаваемых файлов между двумя пользователями
        sc_files_success_recieved               : '805B', //

        // !!! deprecated sc_update_self_nick_avatar_sex          : '805A', // уведомление пользователю о том, что ему следует обновить свои ник, аватар и пол в интерфейсе программы
        
        sc_registered                           : "805E", // уведомление пользователя о том, что он успешно зарегистрирован на сервере
        sc_find_users_list                      : "8058", // список пользователей, в ответ на клиентский запрос поиска пользователей (cs_find_users). Набор объектов
        sc_users_list                           : "8055", // список пользователей, отправляемый на клиент. Набор объектов
        sc_serv_stat                            : '8054', // общая статистика сервера
        sc_private_info                         : '806F', // краткая информация о пользователе, полученная по шаблону

        sc_media_call                           : '8070',
        sc_media_call_accept                    : '8071',
        sc_media_call_reject                    : '8072',
        sc_media_call_close                     : '8075',
        sc_media_call_error                     : '8076',
        sc_media_call_busy                      : '8081',
        sc_media_incoming_call_missed           : '80D7', // пропущен входящий медиазвонок от указанного пользователя, вы в данный момент общались с другим человеком
        sc_media_offer                          : '80D2', // медиа-запрос к клиенту
        sc_media_answer                         : '80D3', // ответ на запрос media offer
        sc_media_ice_candidate                  : '80D4', // ответ на запрос ответ cs_media_ice_candidate
        sc_media_change_settings                : '80D5', // изменение настроек медиа-данных пользователя (аудио/видео)
        sc_media_ready                          : '80D6', // NodeWebKit клиента успешно стартовал и работает

        sc_web_user_foto_file                   : '8074',
        sc_web_support_i_am_busy                : '8087', // если пользователю WEB-поддержки с сайта приходит сообщение не от оператора, с кем он в данный момент говорит, он отвечает этой командой

        sc_adm_set_topic                        : "8079", // результат работы административной команды по установке темы конференции
        sc_adm_list_channels                    : "807A",
        sc_adm_list_users                       : "807B",
        sc_adm_get_server_info                  : "8083", // результат работы административной команды по получению полного списка созданных текстовых конференций. Набор объектов, UID канала
        sc_adm_get_user_profile                 : "8084", // получение полной информации о профиле любого пользователя, административная команда
        sc_adm_list_roles                       : '807F', // список ролей (групп права) пользователей с сервера, административная команда, массив
        sc_adm_user_created                     : '8088', // пользователь только что был создан
        sc_adm_get_job_positions_list           : '8086', // список должностей компании, массив объектов
        sc_adm_server_options                   : '8089', // настройки сервера, ответ на административную команду cs_adm_get_server_options
        sc_adm_server_net_interfaces            : '808A', // список реальных IP адресов сетевых интерфейсов сервера MyChat (IPv4 и IPv6)
        sc_adm_user_info_by_preset              : '808B', // список информационных полей указанного пользователя согласно запрошенному набору
        sc_adm_command_complete                 : '808C', // административная команда успешно выполнена

        sc_adm_job_positions_added              : '808E', // добавлена новая должность
        sc_adm_job_positions_modified           : '808F', // должность была изменена успешно

        sc_adm_role_added                       : '8090', // успешно создана новая группа прав
        sc_adm_role_modified                    : '8091', // группа прав была успешно изменена
        sc_adm_role_users_list                  : '8092', // список пользователей указанной группы прав

        sc_adm_smtp_checked                     : '8093', // SMTP проверка
        sc_invite_to_webchat_sended_ok          : '8094', // уведомление о том, что отправка приглашения для другого человека в MyChat прошла успешно
        sc_adm_websupport_groups                : '8095', // список групп для сотрудников WEB-поддержки сайта, массив
        sc_adm_websupport_group_added           : '8096', // добавлена новая группа WEB-поддержки
        sc_adm_websupport_group_users           : '8097', // список пользователей WEB-support группы
        sc_adm_websupport_group_renamed         : '8098', // группа веб-поддержки успешно переименована
        sc_adm_custom_options                   : '8099', // кастомные настройки, запрошенные с помощью команды cs_adm_get_custom_options
                                                          // Количество полей может быть переменным
        sc_adm_total_contacts_list              : '809A', // общий список контактов, массив объектов

        sc_adm_tcl_group_added                  : '809B', // группа общего списка контактов успешно создана
        sc_adm_tcl_users_added                  : '80A9', // список пользователей успешно занесён в группу общих контактов

        sc_adm_users_imported                   : '809C', // пользователи импортированы в сервер
        sc_adm_server_licenses                  : '809D', // информация о лицензиях MyChat Server

        sc_adm_antiflood_filter_data            : '809E', // информация по антифлуд-фильтрам. Админкоманда
        sc_adm_flood_except_users_list          : '809F', // список пользователей, которых не касаются серверные антифлуд-фильтры, массив. Если массив пустой - значит, таких пользователей на сервере нет

        sc_adm_allow_ip_list                    : '80A0', // список разрешённых IP адресов
        sc_adm_blocked_ip_list                  : '80A1', // список заблокированных IP адресов (масок и диапазонов), массив объектов
        sc_adm_blocked_mac_list                 : '80A2', // список заблокированных MAC адресов
        sc_adm_blocked_nicks_list               : '80A3', // список заблокированных ников, админкоманда. Массив текстовых строк
        sc_adm_blocked_confs_list               : '80A4', // список заблокированных названий конференций. Массив текстовых строк

        sc_adm_bad_words_filter_data            : '80A5', // данные по фильтрам "плохих" слов
        sc_adm_bad_words_list                   : '80A6', // полный список "плохих" слов, админкоманда. Массив текстовых строк
        sc_adm_bad_words_except_confs           : '80A7', // список конференций, в кот орых отключен антимат-фильтр. Массив текстовых строк
        sc_adm_export_users_to_csv_file         : '80A8', // список пользователей с указанными данными успешно экспортирован в CSV файл

        sc_online_users_states                  : '80AA', // список UIN-ов и статусов всех пользователей, которые в данный момент есть онлайн
        sc_dept_list                            : '80AB', // список подразделений компании, массив объектов
        sc_caches                               : '80AC', // контрольные суммы (CRC32) кешей различных данных, которые, возможно, будут нужны клиенту
        sc_adm_all_text_confs                   : '80AD', // список всех текстовых конференций, которые когда-либо создавались на сервере
        sc_just_connect_disconnect_users        : '80AE', // список пользователей, которые отключились или подключились за последнее время (раз в три секунды обычно)
        sc_adm_bans_list                        : '80AF', // список банов пользователей, массив объектов
        sc_adm_moderators_list                  : '80B0', // список модераторов чата, админкоманда, массив объектов
        sc_adm_conf_names_list_by_uid           : '80B1', // список текстовых конференций, которые когда-либо создавались на сервере.

        sc_broadcasts_list_history              : '80B2', // список оповещений, которые были отправлены пользователем, за период. Массив объектов
        sc_put_msg2txt_channel                  : '80B4', // получить сообщение в текстовый канал

        sc_adm_auto_conf_list                   : '80B5', // получить список автосоздаваемых каналов
        sc_adm_auto_conf_created                : '80B6', // успешно добавлена автосоздаваемая конференция. Админкоманда
        sc_txtch_update_modes                   : '80B7', // обновить режимы текстовой конференции
        sc_adm_bbs_msg_list                     : '80B9', // список сообщений на доске объявлений, админкоманда, массив объектов
        sc_adm_bbs_msg_data                     : '80BA', // подробные данные по сообщению на доске объявлений

        sc_adm_ad_users_imported                : '80BB', // список пользователей, которых импортировали из Active Directory
        sc_adm_ad_options                       : '80BC', // настройки по Active Directory интеграции

        sc_adm_update_packet_data               : '80BD', // данные по пакетам обновления для MyChat Client
        sc_adm_online_users_list                : '80BE', // список онлайн-пользователей, которые в данный момент подключены к MyChat Server. Массив объектов
        sc_adm_ad_users_list                    : '80BF', // список пользователей домена для импорта, массив объектов. Количество полей может быть переменным, в зависимости от того, указаны они для данного пользователя
                                      //                                      в домене, или нет. Заблокированные доменные пользователи специально не "вытаскиваются".

        sc_sync_private_history                 : '80C0', // данные для синхронизации истории сообщений в привате. Массив объектов
        sc_adm_script_data                      : '80C1', // запрошенный серверный скрипт со всеми данными
        sc_adm_all_script_states                : '80C2', // список скриптов со статусами, массив объектов
        sc_adm_script_checked                   : '80C3', // проверен синтаксис скрипта
        sc_adm_script_executed                  : '80C4', // скрипт выполнен по запросу, админкоманда
        sc_adm_script_created                   : '80C5', // новый серверный скрипт создан, админкоманда, ответ на 0114 cs_adm_script_create
        sc_adm_script_deleted                   : '80C6', // скрипт только что удалён успешно, пришёл список новых идентификаторов позиций для скриптов (если они есть, если нет - список пустой)
        sc_adm_script_saved                     : '80C7', // скрипт успешно сохранён, админкоманда
        sc_adm_script_log                       : '80C8', // добавить текстовую строчку в лог редактора скриптов

        sc_adm_active_conf_list                 : '80CC', // список работающих конференций, админкоманда, ответ на запрос 011B cs_adm_get_active_conf_list. Массив объектов
        sc_adm_conf_users_list                  : '80CD', // полный список пользователей конференции, админкоманда, ответ на запрос 011D cs_adm_get_users_in_conf. Массив объектов

        // ==== BROADCASTS ==========================================

        sc_broadcast_read_notify                : '8057', // уведомление о прочтении оповещения для пользователя
        sc_broadcast_sent_ok                    : '8100', // оповещение успешно отправлено

        // ==== KANBAN ================================================

        sc_kanban_projects_list                 : '80C9', // список проектов kanban доски, массив объектов, ответ на 0117 cs_kanban_get_projects_list
        sc_kanban_stages_list                   : '80CA', // список этапов kanban-доски, массив объектов
        sc_kanban_tags_list                     : '80CB', // список тегов kanban-доски, ответ на 011A cs_kanban_get_tags_list
        sc_kanban_tasks_list                    : '80CE', // список задач kanban доски. Массив объектов
        sc_kanban_project_created               : '80D0', // новый проект kanban-доски успешно создан
        sc_kanban_stage_created                 : '80D1', // новый этап kanban-доски успешно создан
        sc_kanban_task_created                  : '80E1', // новый проект kanban-доски успешно создан
        sc_kanban_task_comment_added            : '80E2', // новый комментарий к задаче успешно добавлен
        sc_kanban_task_comments_list            : '80E3', // список комментариев к задаче на kanban доске, массив объектов
        sc_kanban_personal_options_list         : '80E5', // список персональных настроек клиента, JSON объект
        sc_kanban_event                         : '80E6', // kanban events
        sc_kanban_project_info                  : '80E7', // информация о kanban-проекте, ответ на запрос 014C cs_kanban_get_project_info
        sc_kanban_task_info                     : '80E8', // информация о задаче на kanban-доске, ответ на запрос 014D cs_kanban_get_task_info (5.21+)
        sc_kanban_last_performers_list          : '80EB', // список последних исполнителей задач на kanban-доске, ответ на запрос 0153 cs_kanban_get_last_performers, массив объектов
        sc_kanban_project_tags_list             : '80FE', // список тегов конкретного проекта
        sc_kanban_dashboard                     : '80FF', // краткая сводная информация по текущим задачам и проектам, массив объектов

        // ====================================================

        sc_personal_options_list                : '80E5', // список персональных настроек клиента, JSON объект

        sc_apply_client_settings                : '80DB', // применить клиентские настройки

        sc_adm_client_options_presets_list      : '80D8', // список шаблонов клиентских настроек, админкоманда, ответ на запрос 122C cs_adm_get_client_options_presets. Массив объектов
        sc_adm_client_options_preset_created    : '80D9', // набор настроек клиента успешно создан, ответ на команду 122E cs_adm_create_client_options_preset
        sc_adm_client_options_preset_updated    : '80DA', // набор настроек успешно отредактирован

        sc_adm_new_user_group_created           : '80DD', // уведомление о том, что новая группа пользователей успешно создана
        sc_adm_users_groups_list                : '80DE', // список групп пользователей, масив объектов
        sc_adm_group_users_list                 : '80DF', // список пользователей в группе, массив объектов

        sc_adm_integration_tool_options         : '80E0', // настройки интеграции инструмента (phpbb... etc.)
        sc_adm_get_check_domains_list           : '80E4', // список доменов с адресами, пользователи которых могут подключаться к серверу, массив объектов

        sc_adm_allowed_and_blocked_plugins      : '80E9', // список разрешённых и запрещённых клиентских плагинов
        sc_adm_script_info                      : '80EA', // краткая информация о скрипте, ответ на 0152 cs_adm_get_script_info

        // ==== FORUM ================================================

        sc_forum_structure                        : '80EF', // структура форума MyChat, ответ на запрос 0156 cs_forum_get_structure
        sc_forum_section_or_topic_created         : '80F0', // раздел форума или тема успешно создан
        sc_forum_post_created                     : '80F2', // сообщение в форуме успешно создано
        sc_forum_section_or_topic_changed         : '80F3', // раздел или тема форума успешно изменены
        sc_forum_section_or_topic_or_post_deleted : '80F4', // раздел, тема форума или сообщение успешно удалены
        sc_forum_post_changed                     : '80F5', // пост форума изменён
        sc_forum_topic_posts                      : '80F6', // список сообщений темы форума. Массив объектов
        sc_forum_section_info                     : '80F7', // информация о секции (список топиков, краткая информация о них: LastChange, UIN, DisplayName, Sex, Avatar, CountMessages). Массив объектов

        // ==========================================================

        sc_command_complete                     : '80F1', // команда успешно выполнена
        sc_book_data                            : '80FB', // возвращает содержимое справочника в ответ на команду cs_get_book

        sc_file_exists                          : '80FC', // есть или нет файл с таким хэш-кодом на сервере
        sc_image_thumbs                         : '80FD', // уменьшенная копия изображения с сервера

        sc_token_created                        : '80ED', // создан токен для доступа к сервису чата

        sc_adm_backup_server_db_ok              : '8101', // резервная копия баз данных сервера успешно создана
        sc_hide_text_conf                       : '8102', // скрыть конференцию из списка доступных
        sc_adm_integration_telegram_users_list  : '8103', // список пользователей для интеграции с Telegram. Массив объектов
        sc_adm_integration_telegram_user_added  : '8104', // уведомление, только что добавлен новый мобильный юзер в таблицу Telegram-пользователей
        sc_removed_user_info                    : '8105', // краткая информация об удалённом пользователе
        sc_login_finish                         : '8106', // клиентское приложение успешно авторизовано на сервере и ему передана вся необходимая информация (права, join-ы в конференции и т.п.)

        sc_adm_history_info_private             : '8107', // админкоманда, информация по сообщениям в привате за диапазон времени, ответ на 0173 cs_adm_get_history_info_private
        sc_adm_history_private                  : '8108', // лог приватных сообщений двух пользователей по указанному диапазону индексов, админкоманда, ответ на 0174 cs_adm_get_history_private
        sc_adm_history_info_conf                : '8109', // админкоманда, информация по сообщениям в конференции за диапазон времени, ответ на 0175 cs_adm_get_history_info_conf
        sc_adm_history_conf                     : '810A', // лог приватных сообщений двух пользователей по указанному диапазону индексов, админкоманда, ответ на 0176 cs_adm_get_history_conf
        sc_adm_history_private_user_pairs       : '810B', // админкоманда, все пары приватных разговоров за все время, ответ на 0177 cs_adm_get_history_private_user_pairs

        sc_sync_conf_history                    : '810D', // данные для синхронизации истории сообщений в конференции
        sc_info                                 : '810E', //

        sc_adm_clear_console                    : '810F', // очистить консоль вывода данных в редакторе скриптов
        sc_adm_blocked_hwid_list                : '8110', // список заблокированных HardwareID адресов
        sc_adm_system_logs                      : '8111', // системные логи за период времени, ответ на 0182 cs_adm_get_system_logs

        sc_web_services_info                    : '8114', // информация о WEB-сервисах MyChat Server. Присылается, только если они перенастроены либо изменяются в данный момент
        sc_server_ports                         : '8115', // список сетевых портов MyChat сервер, отправляется клиенту, если меняются сетевые настройки MyChat Server

        sc_adm_domains_list                     : '8116', // список доменов, с которыми может работать сервер MyChat
        sc_adm_forum_subs_groups_list           : '8117', // список групп пользователей, которые подписаны на конкретную тему либо секцию
        sc_adm_forum_subs_users_list            : '8118', // список идентификаторов пользователей, которые подписаны на конкретную тему либо секцию

        sc_forum_subs                           : '8119', // список подписок на темы и разделы форума
        sc_custom_options                       : '811A', // кастомные настройки клиента, запрошенные с сервера командой cs_set_custom_options

        sc_adm_integration_quiz_check_question  : '811B', // проверка существования вопроса в базе робота-викторины
        sc_adm_integration_quiz_question_added  : '811C', // новый вопрос в базу робота-викторины добавлен успешно
        sc_adm_integration_quiz_stat            : '811D', // статистика робота-викторины

        sc_exec_func_result                     : '811E', // результат выполнения функции на сервере
        sc_users_live_search                    : '811F', // список пользователей на сервере по заданному шаблону, результат поиска команды 019A cs_users_live_search

        sc_private_dialogs                      : '8120', // список приватных диалогов пользователя, массив объектов, свежие - сначала
        sc_del_private_dialog                   : '8121', // удалить диалог с указанным юзером из списка приватных диалогов, уведомление об успешном удалении на сервере, рассылается всем пользователям-владельцам

        sc_private_read                         : '8122', // уведомление самому себе и всем инстансам себя (multilogin) о том, что указанное приватное сообщение прочитано
        sc_private_read_notify                  : '8123', // уведомление оппоненту, что его собеседник только что прочитал приватное сообщение с указанным индексом (и все приватные сообщения до него)
        sc_private_got                          : '8124', // уведомление самому себе и всем инстансам себя (multilogin) о том, что указанное приватное сообщение получено
        sc_private_got_notify                   : '8125', // уведомление оппоненту, что его собеседник только что получил приватное сообщение с указанным индексом (и все приватные сообщения до него)
        sc_private_dialog_msg_states            : '8126', // информация о полученных и прочитанных сообщениях в приватном диалоге с оппонентом

        sc_private_redirect_complete            : '8127', // диалог успешно передан, приват можно закрыть
        sc_users_list_for_redirect_dialog       : '8128', // список пользователей для редиректа
        sc_my_multicast_msg                     : '8129', // (7.4+) моё многоадресное сообщение, ответ от сервера

        sc_create_presenter                     : '812A', // ответ на создание презентера
        sc_adm_terminal_command_result          : '812B', // ответ на терминальную команду админки


        sc_remove_conversations                 : '8112', // полностью удалить указанные протоколы разговоров за всё время из локальной базы данных
        sc_tech_kick                            : '812C', // принудительно выйти из текстовой конференции, на сервер ничего не отправлять
        sc_common_options                       : '812D', // команда для обновления общих для всех пользователей сервера параметров,
                                                          // все переменные - необязатльные, применять настройку, только если поле реально есть внутри JSON

        sc_adm_allow_ip_added                   : '812E', // добавлен IP, маска или диапазон в фильтре разрешённых IP адресов
        sc_adm_blocked_ip_added                 : '812F', // добавлен IP, маска или диапазон в фильтре запрещённых IP адресов
        sc_adm_script_log_clear                 : '8130', // очистить лог редактора скриптов в админке

        sc_kanban_task_taken                    : '8131', // задача успешно взята в работу

        sc_users_changeid_data                  : '8132', // информация о списке пользователей, UIN, ChangeID + данные
        sc_users_avatars                        : '8133', // аватары пользователей (прямоугольные картинки), массив объектов

        sc_cids_killed                          : '8137', // только что отключен от сервера список CID-ов пользователей
        sc_cids_halted                          : '8138', // только что отключен от сервера + завершены приложения списка CID-ов пользователей

        sc_conf_read                            : '8139', // пользователь только что прочитал сообщение в конференции, команда рассылается  всем инстансам UIN при мультилогине
        sc_media_incoming_call_accepted_by_another_cid: '813A', // медиа-звонок принят мной на другом устройстве
        sc_close_broadcast_window               : '813B', // закрыть окно входящего оповещения, оповещения были прочитаны на другом устройстве

        sc_adm_asterisk_log                     : '8142', // свежий кусок лога работы с Asterisk
        sc_integrations                         : '8143', // список включенных интеграций, массив объектов

        sc_adm_integration_turbosms_balance     : '8147', // баланс состояния счёта в сервисе TurboSMS
        sc_adm_integration_turbosms_ping        : '8148', // ответ PING от сервиса TurboSMS
        sc_adm_integration_turbosms_send_message: '8149', // ответ на отправку тестового сообщения

        sc_media_call_start_ok                  : '814A', // начат медиа звонок с указанными параметрами

        sc_tech_message                         : '80EE', //

        sc_personal_users_groups_list           : '814C', // список личных групп пользователей, со списком юзеров внутри, массив объектов
        sc_adm_new_personal_user_group_created  : '814D', // уведомление о том, что новая персональная группа пользователей успешно создана
        sc_adm_personal_group_users_list        : '814E', // список пользователей в персональной группе юзера, массив объектов
        sc_new_personal_user_group_created      : '814F', //  уведомление о том, что новая персональная группа пользователей успешно создана
        sc_personal_user_group_data             : '8151', // личная группа пользователя, ID, название и список пользователей
        sc_users_from_personal_user_group_removed: '8152', // список пользователей из персональной группы успешно удалён
        sc_personal_users_groups_names_list     : '8153', // список личных групп пользователей, массив объектов
        sc_public_users_groups_list             : '8155', // список публичных групп пользователей, массив объектов
        sc_kanban_rights_group_list             : '8156', // список групп прав с доступом к канбану, массив объектов
        sc_kanban_user_group_added_to_project   : '8157', // группа успешно добавлена в проект
        sc_kanban_project_group_list            : '8158', // список групп доступа к канбан-проекту
        sc_kanban_removed_user_groups_from_project: '8159', // из канбан-проекта удалены указанные группы пользователей
        sc_common_user_group_data               : '815A', // общая группа пользователя, ID, название и список пользователей
        sc_all_user_group_membership_list       : '815B', // список общих и личных групп, куда входит указанный пользователь, ответ на запрос cs_adm_get_all_user_group_membership_list

        sc_get_geo_position                     : '815C', // получить географическую позицию устройства пользователя ответ - 01DA cs_geo_position (8.12+)
        sc_user_gps_history                     : '815D', // история перемещений пользователя за период, ответ на запрос 01DB cs_get_user_gps_history

        sc_users_fired                          : '815E', // пользователи успешно уволены, ответ на cs_adm_fire_users
        sc_remove_history                       : '815F', // удаление сообщений/файлов/логов/записей звонков и т.п. любого контента в MyChat Client за период или полностью
                                                          // команда может приходить по запросу cs_get_delayed_commands либо, если клиент онлайн, то в момент выполнения на сервере
        sc_adm_custom_menu_created              : '8162', // custom-меню для клиентских приложений создано успешно, ответка на 01DF cs_adm_custom_menu_create
        sc_adm_custom_menu_list                 : '8163', // полный список custom-меню для клиентских приложений, массив объектов, ответка на 01E0 cs_adm_get_custom_menu_list
        sc_custom_menu                          : '8164', // custom-menu пользователя ответ на 01E5 cs_get_custom_menu
        sc_set_remote_client_current_place_id   : '8145', // открыть в интерфейсе удалённого клиента приват с определённым человеком либо конференцию

        sc_media_conf                           : '8160', // работа с медиаконференциями, основные команды с большой буквы, дополнительные - с маленькой
        sc_media_conf_join                      : '8166', // подключиться к медиаконференции
        sc_media_conf_leave                     : '8167', // отключиться от медиаконференции

        sc_adm_ad_users_photos                  : '8168', // отмашка в админку, что с запросами фоток мы закончили, можно начинать спрашивать, либо информация о наличии фоток (контрольные суммы, логины)
                                                          // массив объектов
        sc_adm_ad_user_photo_thumb              : '8169', // ответ от сервера, миниатюра фотографии пользователя
        sc_remove_my_acc                        : '816B', //

        sc_conf_status_change                   : '816C', // изменён статус конференции

        sc_adm_check_user_pwd_state             : '816D', // ответ на запрос корректности пароля пользователя (cs_adm_check_user_pwd)
        sc_media_conf_user_list                 : '816E', // ответ на запрос списка пользователей медиа-конференции (cs_get_media_conf_user_list)
        sc_media_conf_user_joined               : '816F', // пользователь подключился к медиаконференции, команда отправляется всем онлайн-пользователям указанной конференции
        sc_media_conf_broadcast                 : '8170', // универсальная команда рассылки сообщения всем пользователям медиа конференции, кроме отправителя

        sc_conf_user_list                       : '8171', // список пользователей указанной конференции, ответ на cs_get_conf_user_list
        sc_remove_users                         : '8172', // с сервера удалены указанные пользователи
        sc_join_newbie_to_txt_channel           : '8173', // замена sc_join_newbies_txt_channel

        sc_all_users_changed                    : '80F8', // информирование всех подключенных клиентов о том, что все юзеры изменились. Как правило, поменялся DisplayName. Следует спросить у сервера
                                                          // новую информацию обо всех пользователях, которые интересны клиентам (открытые конференции и приваты).
        sc_important_notifiers                  : '8174', // важное уведомление с сервера, нужно сгенерировать локально
        sc_adm_user_conf_list                   : '8175', // список конференций, в которые входит пользователь, ответ на запрос cs_adm_get_user_conf_list, массив объектов

        sc_conf_removed                         : '8176', // текстовая конференция удалена, нужно удалить кеши и всю локальную историю
        sc_badword_list_uploaded                : '8177', // список "плохих" слов для фильтра успешно добавлен


        // ==== Server -> Node =================================================

        sn_config                               : '1',
        sn_start                                : '2',
        sn_stop                                 : '3',
        sn_quit                                 : '4',
        sn_invite                               : '5',
        sn_iApi                                 : '6',
        sn_ip_filter                            : '7',
        sn_iApi_key                             : '8',
        sn_https_error                          : '9', 
        sn_rest_request                         : '10',
        sn_icq_connect                          : '11',
        sn_icq_send_message                     : '12',
        // sn_get_backup_path                      : '13', // sn_path_info
        sn_info                                 : '14',
        sn_path_info                            : '15',
        sn_operators                            : '16',
        sn_support_options                      : '17',
        sn_update_options                       : '18',
        sn_check_token                          : '19',
        sn_run_command                          : '20',
        sn_check_group_invite                   : '21',
        
        sn_media_conf_create                    : '22',
        sn_media_conf                           : '23',
        sn_media_conf_join                      : '24',
        sn_media_conf_leave                     : '25',
        sn_user_conf_info                       : '26',
        sn_media_conf_close                     : '27', // принудительное удаление медиа-конференции

        sn_check_ssl_info                       : '28', // перевірка сертифіката

        // ==== Node -> Server =================================================

        ns_started                              : '1001',
        ns_stopped                              : '1002',
        ns_bye                                  : '1003',
        ns_check_invite                         : '1004',
        ns_invite_sent                          : '1005',
        ns_integrationAPI                       : '1006',
        ns_ip_filter                            : '1007',
        ns_iApi_key                             : '1008',
        ns_https_error                          : '1009',
        ns_rest_request                         : '1010',
        ns_icq_connected                        : '1011',
        ns_icq_contact_list                     : '1012',
        ns_icq_send_message                     : '1013',
        ns_get_by_groups                        : '1015',
        ns_get_by_uins                          : '1016',
        ns_support_options                      : '1017',
        ns_check_token                          : '1018',
        ns_remove_tokens                        : '1019',
        ns_private_message                      : '1020',
        ns_check_group_invite                   : '1021',

        ns_media_conf                           : '1022',
        ns_media_conf_created                   : '1023',
        ns_media_conf_error                     : '1024',
        
        ns_get_user_conf_info                   : '1025',
        ns_media_conf_closed                    : '1026',
        ns_media_conf_leave                     : '1027',

        ns_check_ssl_info                       : '1028',

        // == HELP PATH ==========================================================

        accordance : {
                "navServerInfo/ServerInfo"     : "/serverhelp/#{lang}/index.html?serverinfo.htm",
                "navServerInfo/OnlineUsers"    : "/serverhelp/#{lang}/index.html?infoonlineusers.htm",
                "navServerInfo/NssService"     : "/serverhelp/#{lang}/index.html?infoadditionalservices.htm",
                "navServerInfo/Forum"          : "/serverhelp/#{lang}/index.html?infomychattechforum.htm",
                "navServerInfo/UserProfile"    : "/serverhelp/#{lang}/index.html?userprofile.htm",
                "navServerInfo/Terminal"       : "/serverhelp/#{lang}/index.html?infomychatterminal.htm",
                "navUsers/UserManager"         : "/serverhelp/#{lang}/index.html?usermanagement.htm",
                "navUsers/UserProfile"         : "/serverhelp/#{lang}/index.html?userprofile.htm",
                "navUsers/WorkPositions"       : "/serverhelp/#{lang}/index.html?userdepartments.htm",
                "navUsers/Contacts"            : "/serverhelp/#{lang}/index.html?usercommoncontacslist.htm",
                "navUsers/UsersImports"        : "/serverhelp/#{lang}/index.html?userimportfromfile.htm",
                "navUsers/Operators"           : "/serverhelp/#{lang}/index.html?usermoderators.htm",
                "navUsers/Bans"                : "/serverhelp/#{lang}/index.html?userbanlist.htm",
                "navUsers/ActiveDirectory"     : "/serverhelp/#{lang}/index.html?userimportactivedirectory.htm",
                "navUsers/UserGroups"          : "/serverhelp/#{lang}/index.html?usersgroups.htm",
                "navUsers/AdDomains"           : "/serverhelp/#{lang}/index.html?", // нет, синхронизация
                "navConference/AutoChannels"   : "/serverhelp/#{lang}/index.html?conferenceauto.htm",
                "navConference/ActiveChannels" : "/serverhelp/#{lang}/index.html?conferencesonlinelist.htm",
                "navRights/GroupRights"        : "/serverhelp/#{lang}/index.html?grouprights.htm",
                "navRights/GroupUsers"         : "/serverhelp/#{lang}/index.html?grouprightsusers.htm",
                "navRights/PluginAccess"       : "/serverhelp/#{lang}/index.html?", // нет
                "navTools/WebSupport"          : "/serverhelp/#{lang}/index.html?websupport.htm",
                "navTools/MyChatGuest"         : "/serverhelp/#{lang}/index.html?mychatguest.htm",
                "navTools/History"             : "/serverhelp/#{lang}/index.html?logsviewer.htm",
                "navTools/BBS"                 : "/serverhelp/#{lang}/index.html?", // нет
                "navTools/Updates"             : "/serverhelp/#{lang}/index.html?mychatupdatesystem.htm", // +
                "navTools/MCScripts"           : "/serverhelp/#{lang}/index.html?scripts.htm",
                "navTools/Reboot"              : "/serverhelp/#{lang}/index.html?", // нет
                "navTools/PresetManager"       : "/serverhelp/#{lang}/index.html?optionspresetscreate.htm", // +
                "navTools/RemoteSettings"      : "/serverhelp/#{lang}/index.html?optionspresetsassign.htm", // +
                "navTools/Integrations"        : "/serverhelp/#{lang}/index.html?toolsintegrations.htm", // +
                "navTools/Relocation"          : "/serverhelp/#{lang}/index.html?", // нет
                "navTools/Bots"                : "/serverhelp/#{lang}/index.html?", // нет
                "navTools/TotalClear"          : "/serverhelp/#{lang}/index.html?", // нет
                "navTools/Testport"            : "/serverhelp/#{lang}/index.html?MyChatServerTestPortErrors.htm",
                "navFilters/FilterAntiflood"   : "/serverhelp/#{lang}/index.html?filtersantiflood.htm",
                "navFilters/FilterBadWords"    : "/serverhelp/#{lang}/index.html?filtersbadwords.htm",
                "navFilters/FilterIP"          : "/serverhelp/#{lang}/index.html?filtersip.htm",
                "navFilters/FilterMAC"         : "/serverhelp/#{lang}/index.html?filtersmac.htm",
                "navFilters/FilterNames"       : "/serverhelp/#{lang}/index.html?filtersnames.htm",
                "navFilters/FilterHardware"    : "/serverhelp/#{lang}/index.html?filtershardwareid.htm",
                "navSettings/SettingsNetwork"  : "/serverhelp/#{lang}/index.html?networkoptions.htm",
                "navSettings/SettingsInfo"     : "/serverhelp/#{lang}/index.html?serverinfooptions.htm",
                "navSettings/SettingsLogs"     : "/serverhelp/#{lang}/index.html?logoptions.htm",
                "navSettings/SettingsFTP"      : "/serverhelp/#{lang}/index.html?ftpoptions.htm",
                "navSettings/SettingsWEB"      : "/serverhelp/#{lang}/index.html?webservicesoptions.htm",
                "navSettings/SettingsBackup"   : "/serverhelp/#{lang}/index.html?backupoptions.htm",
                "navSettings/SettingsService"  : "/serverhelp/#{lang}/index.html?maintenanceoptions.htm",
                "navSettings/SettingsAPI"      : "/serverhelp/#{lang}/index.html?integrationapioptions.htm",
                "navSettings/SettingsSMTP"     : "/serverhelp/#{lang}/index.html?smtpoptions.htm",
                "navSettings/SettingsFolders"  : "/serverhelp/#{lang}/index.html?mychatserverfolders.htm",
                "navSettings/Others"           : "/serverhelp/#{lang}/index.html?additionaloptions.htm",
                "navLicense/LicenseMyChat"     : "/serverhelp/#{lang}/index.html?mychatserverlicenses.htm",
                "navLicense/LicenseGuest"      : "/serverhelp/#{lang}/index.html?mychatguestlicenses.htm",
                "navLicense/LicenseClear"      : "/serverhelp/#{lang}/index.html?mychatclearlicenses.htm", // +
                "navLicense/LicenseQuiz"       : "/serverhelp/#{lang}/index.html?mychatquizlicenses.htm",  // +
                "mychat_versions"              : "/serverhelp/#{lang}/index.html?versionshistory.htm",

        },

        // ==== Client Type ====================================================

        ClientType: {
            WIN32           : 'win32',
            FTP             : 'ftp',
            WEB_ADMIN       : 'admin',
            WEB_KANBAN      : 'kanban',
            WEB_CHAT        : 'web',
            WEB_SUPPORT     : 'websupport',
            COMMON          : 'common',
            INTEGRATION_API : 'integrationapi',
            WEB_FORUM       : 'forum',
            WIN32_NEXT      : 'win32next',
            MACOS           : 'macos',
            ANDROID         : 'android',
            LINUX           : 'linux',
            GUEST           : 'mcguest',
            IOS             : 'ios',
            UPDATES         : 'updates',

            ALL             : 'any',
            UNKNOWN         : 'unknown'
        },

        callType : {
            VOICE  : 0,
            VIDEO  : 1,
            SHARING: 2
        },

        PrivateTask: {
            DEFAULT                  : 0,
            SEND_FILES               : 1,
            SEND_FILES_FROM_EXPLORER : 2,
            VOICE_CALL               : 3,
            VIDEO_CALL               : 4,
            MY_SCREEN_SHARING        : 5
        },

        serverLogLevel: {
            NOTHING         : 0, // ничего не логгировать
            SERVER_ERRORS   : 10, // серверные ошибки
            NETWORK_ERRORS  : 20, // сетевые ошибки
            SERVER_INFO     : 30, // информационные сообщения о работе сервера

            CONNECTIONS     : 800, // подключения и отключения пользователей
            NETWORK         : 900, // входящий трафик
            VERBOSE         : 1000 // полный трафик от сервера к клиентам и обратно
        },

        serverLogEvents: {
            MYCHAT_TRAFFIC_IN  : 1,
            MYCHAT_TRAFFIC_OUT : 2,
            ALL_INPUT_TRAFFIC  : 3
        },

        userStates: {
            offline    : -1,
            online     : 0,
            away       : 1,
            dnd        : 2,
            webOnline  : 3
        },

        // ==== User Rights ====================================================

        RS: {
            QLogin: 1,                                            //"811":"Подключаться к MyChat серверу",
       /*+*/QBBSPost: 2,                                          //"786":"Создание новых сообщений на доске",
       /*+*/QBBSView: 3,                                          //"785":"Просмотр доски объявлений",
       /*+*/QTxtChCreate: 4,                                      //"797":"Создание текстовых каналов",
       /*+*/QTxtChSay: 5,                                         //"844":"Отправка сообщений в текстовые каналы",
       /*+*/QTxtChJoin: 6,                                        //"796":"Вход в существующие текстовые каналы",
       /*+*/QPrivateOpen: 7,                                      //"831":"Открытие новых приватов",
       /*+*/QPrivatesGetMessages: 8,                              //"832":"Получение приватных сообщений от других пользователей",
       /*+*/QUserDetailsChange: 9,                                //"837":"Изменение персонального профиля",
       /*+*/QFilesSend: 10,                                       //"841":"Разрешить отправлять файлы другим пользователям",
       /*+*/QFilesReceive: 11,                                    //"842":"Разрешить принимать файлы от других пользователей",

            QOperatorBan: 12,                                     //"821":"Право наказывать пользователей (ban)",
            QOperatorTurnOutFromChannel: 13,                      //"820":"Право изгнания пользователей из текстовых каналов (kick)",
            QKill: 14,                                            //
            QBlockIP: 15,                                         //
            QBlockMAC: 16,                                        //
            QOperatorTxtChTopicSet: 17,                           //"818":"Изменение темы текстовых каналов",

       /*+*/QFTPPublic: 18,                                       //"840":"Доступ к общей папке на файловом сервере",
       /*+*/QFTPPersonal: 19,                                     //"839":"Доступ к личной папке на файловом сервере",

            QRemoteAdm: 20,                                       //"783":"Удаленное администрирование сервера через консоль MyChat Client",
            QOperatorClearChannelText: 21,                        //"819":"Очистка текстовых каналов у всех пользователей",

       /*+*/QContactsListChange: 22,                              //"799":"Изменение персональной панели контактов",
       /*+*/QBroadcasts: 23,                                      //"823":"Отправка оповещения другим пользователям",

       /*-*/QAllowPersonalMsg: 24, //trash                               //"794":"Отправка персональных сообщений в каналах",
       /*-*/QAllowAlertMsg: 25,    //trash                               //"795":"Отправка важных персональных сообщений в каналах",
            QAllowSysInfoView: 26,                                //"815":"Просмотр сетевого имени компьютера и IP-адреса пользователя",

       /*+*/QSendImages: 27,                                      //"792":"Вставка изображений",

            QAllowIgnores: 28,                                    //"805":"Игнорирование других пользователей",

            QAllowInvites: 29,                                    //"810":"Отправка приглашений в каналы другим пользователям",

       /*+*/QImagesPrivates: 30,                                  //"830":"Вставка изображений",
       /*+*/QShowCommonContactsList: 31,                          //"800":"Показывать общий список контактов",
       /*+*/QHalt: 32,

            QTxtChRename: 33,
            QTxtChDelete: 34,
         // QForciblyInvites: 35,                                 // deprecated
            QBlockUIN: 36,
            QAllowPlugins: 37,                                    //"827":"Использование плагинов",

       /*+*/QAllowOptionsChange: 38,                              //"806":"Изменение настроек клиентского приложения",

       /*-*/QAllowMyFavoriteChannels: 39, //trash                        //"793":"Доступ к ''Моим любимым каналам''",

       /*+*/QAllowCloseProgram: 40,
       /*+*/QAllowContactsListsTotal: 41,                         // разрешить или запретить полностью доступ к панели контактов
       /*+*/QAllowAccountsManager: 42,                            // разрешить пользоваться менеджером учётных записей
            QWEBAllowViewLogsSelfPrivates: 43,                    // просмотр своих разговоров в приватах
            QWEBAllowViewLogsAllChannels: 44,                     // просмотр всех разговоров в каналах
            QWEBAllowViewLogsAllPrivates: 45,                     // просмотр всех разговоров в приватах
       /*+*/QAllowFindUsersTool: 46,                              // разрешить поиск пользователей

       /*-*/QAllowActions: 47,    //trash                                // разрешить вставку в текст Action-ов
            QAllowClientLogPrivates: 48,                          // разрешить или запретить клиенту писать логи приватов локально, у себя на компьютере
       /*-*/QMessengerStyle: 49,                                  // Messenger-style отображения майчат клиента

            QAllowOpenComputer: 50,                               // показывать или нет в меню опцию "Открыть компьютер" по щелчку на имени юзера
            QExceptionAllowReceivePrivates: 51,                   // Приём приватных сообщений от пользователей, которым запрещена отправка приватных сообщений
            QAllowViewRemoteUserMACAddress: 52,                   // разрешить смотреть MAC адрес удалённого юзера
       /*+*/QAllowViewRemoteUserActiveProcess: 53,                // разрешить смотреть активный процесс удалённого юзера
                
       /*-*/QWEBAllowViewFTPLog: 54,                              // Просмотр FTP протоколов

       /*+*/QWEBAllowViewSystemLog: 55,                           // Просмотр системных протоколов

       /*-*/QEnableTaskWorkersJobTimeControlSystem: 56,           // контроль рабочего времени
            QWEBSupportAllowAccess: 57,                           // доступ к WEB-интерфейсу веб-суппорта

       /*+*/QWEBAllowAdmin: 58,                                   // администрирование сервере через WEB

            QAllowProgramBlockTool: 59,                           // Блокирование работающей программы паролем
            QAllowNetworkStatusChange: 60,                        // Изменение сетевого статуса
            QEnableAllowedClientsPluginsList: 61,                 // Активировать список разрешённых клиентских плагинов
            QEnableBlockedClientsPluginsList: 62,                 // Активировать список запрещённых клиенских плагинов
            QEnablePluginsManagement: 63,                         // Разрешить управление плагинами на клиенте

            QAllowImagesInBroadcasts: 64,                         // вставка картинок в оповещениях

        /*-*/QEnableFontLayoutsInMessages: 65,                     // использование шрифтовой разметки в отправляемых сообщениях
            QAllowCreateHiddenChannels: 66,                       // - (это ещё не внесено в базу данных) разрешить создавать скрытые каналы
        /*-*/QEnableTransferFilesOnlyViaServer: 67,                // передавать файлы только через сервер
            QEnableImmunityFromBans: 68,                          // иммунитет от банов

       /*- ?? */QEnableAdvertBlock: 69,                               // показывать или нет рекламный блок
       /*+*/QBlockPrivatesExceptPersonalContacts: 70,             // Запретить приваты со всеми, кроме входящих в личный список контактов
       /*+*/QBlockPrivatesExceptCommonContacts: 71,               // Запретить приваты со всеми, кроме входящих в общий список контактов
       /*+*/QEnableImmunityFromIgnores: 72,                       // Иммунитет от игноров

            QIntegrationAPIEnableSendMsgFromAnyUser: 73,          // Разрешить отправку сообщений от имени этих пользователей

       /*+*/QEnableVoiceCalls: 74,                                // Голосовые звонки
       /*+*/QEnableVideoCalls: 75,                                // Видеозвонки
       /*+*/QAllowGetVoiceCalls: 76,                              // Разрешить получать голосовые звонки
       /*+*/QAllowGetVideoCalls: 77,                              // Разрешить получать видеозвонки
                
            QAllowAddNewPlugins: 78,                              // Добавление новых плагинов
            QAllowDeletePlugins: 79,                              // Удаление плагинов

       /*+*/QAllowChangeAccountPassword: 80,                      // Изменение пароля своей учётной записи
            QAllowAccessForeignersToPersonalFiles: 81,            // Разрешить доступ к личным файлам для других пользователей ("только чтение")
            QAllowPublishLinkToPersonalFiles: 82,                 // Разрешить публиковать ссылку на "Мои файлы на сервере"

            QFTPPublicWrite: 83,                                  // разрешить писать в общую папку
            QFTPPersonalWrite: 84,                                // разрешить писать в личную папку
            QAllowSendInvites: 85,                                // разрешить отправлять приглашения в MyChat другим людям
            QWEBAllowViewNodeJSLogs: 86,                          // разрешить просмотр логов NodeJS сервера
            QWEBAllowViewAuditLogs: 87,                           // разрешить просмотр логов аудита

       /*-*/QCommonAllowMainMenu: 144,                            // главное меню программы
       /*-*/QCommonAllowDialogs: 145,                             // панель диалогов

            QForumAccess: 146,                                    // доступ ко встроенному форуму
            QForumCreateSections: 147,                            // форум, "Можно создавать новые разделы"
            QForumChangeSections: 148,                            // форум, "Изменение разделов (удаление, переименование, перенос)"
            QForumCreateTopics: 149,                              // форум, "Можно создавать новые темы"
            QForumChangeTopics: 150,                              // форум, "Изменение тем (удаление, переименование, перенос)"
            QForumCreatePosts: 151,                               // форум, "Можно создавать новые сообщения"
            QForumChangePosts: 152,                               // форум, "Изменение сообщений (удаление, модификация, перенос)"
            QForumChangeSelfPosts: 153,                           // форум, "Изменение собственных сообщений (удаление, модификация, перенос)"
            QForumDeleteSections: 154,                            // форум, "Удаление секций"
            QForumDeleteTopics: 155,                              // форум, "Удаление тем"
            QForumDeletePosts: 156,                               // форум, "Удаление сообщений"
            QForumDeleteSelfPosts: 157,                           // форум, "Удаление собственных сообщений"

            QBroadcastsOnlyInMyGroups: 158,                       // оповещения, "Отправка оповещений только в пределах своих групп пользователей"
            QTxtChViewHidden : 159,                               // Конференции, "Просмотр скрытых конференций"
            QICQAccess: 160,                                      // "Интеграция с ICQ", "Доступ к ICQ"

         /*+*/QTxtConfSendFiles: 161,                               // "Конференции", "Вставка файлов"
         /*+*/QPrivSendMessages: 162,                               // "Приваты", "Отправка сообщений"
         /*+*/QPrivInsertFiles: 163,                                // "Приваты", "Вставка файлов"
                
            QAllowRemoveConfMessages: 164,                        // "Администрирование", "Удаление любых сообщений в конференциях"
            QAdmAccessToRemoveConfMessages: 165,                  // "админка", "Инструменты", "Просмотр протоколов", Удаление сообщений из протоколов конференций
            QAllowViewHistoryTool: 166,                           // Просмотр истории сообщений

       /*+*/QAllowViewRemoteUserProfile: 174,                     //  "Профиль", "Просмотр профилей других пользователей"
            QAllowFilterHardware: 175,
            QAllowSettingsFolders: 176,
            
            QAdmAccessLicensesWipe: 177,                          // админка, "Доступ к функциям управления сервером", "Лицензии", "Очистка данных"
            QAdmToolWipe: 178,                                    // админка, доступ к инструменту "Очистка данных"
            QEnableScreenSharing: 179,                            // "Приваты", Трансляция экрана
            QAllowGetScreenSharing: 180,                          // "Приваты", Разрешить получать трансляции экрана
            
            QForumChangeTopicsColor : 181,                        // "Форум", "Изменение цвета для тем"
            QForumSubsNotify        : 182,                        // "Форум", "Подписка/отписка на уведомления в секциях и темах"
            QForumInsertImages      : 183,                        // "Форум", "Вставка изображений в сообщениях"
            QForumInsertFiles       : 184,                        // "Форум", "Вставка файлов в сообщениях"
            QAdmAccessForumSubs     : 185,                        // "Форум", "Управление подписками"

            QAdmAccessLicensesQuiz  : 187,                        // админка, "Доступ к функциям управления сервером", "Лицензии", "MyChat викторина"
            QAdmAccessToRemovePrivMessages  : 188,                // "админка", "Инструменты", "Просмотр протоколов", Удаление приватных сообщений из протоколов"
            QCommonAllowScreenshotTool      : 189,                // Отправка снимка экрана в чат

       /*+*/QAllowRemovePrivateMessages     : 190,                // Удаление любых сообщений в приватах
       /*+*/QCommonAllowQuickMessages       : 191,                // "Быстрые" сообщения
       /*+*/QAllowRedirectDialogs           : 192,                // Перенаправление диалогов
                
            QMulticast : 193,                                     // "Приваты", "Многоадресные приватные сообщения"
            QAdmTerminal: 194,                                    // "MyChat Server", "Terminal"
            QForumPresenter : 195,                                // "Форум", "Создание ссылок для просмотра тем и сообщений"
            QKanbanFullAccess: 196,                               // полный доступ к Kanban доске
            QEnableWakeUpBeep: 197,                               // Разрешить "будить" пользователя
            QAllowRemoveLocalDBConfMessages: 198,                 // "Конференции", "Удаление любых сообщений в конференциях в своей локальной базе данных"
            QAllowRemoveLocalDBPrivateMessages: 199,              // "Приваты", "Удаление любых сообщений в приватах в своей локальной базе данных"
            QAllowClientLogConferences: 200,                      // "Протоколирование", "Хранить историю сообщений конференций в локальной базе данных"
            QAndroidSecurityMode: 201,                            // "Android", "Запрет скриншов экрана в диалогах и blur окна приложения при переключении между последними приложениями"
            QAndroidShowCallsOnMainScreen: 202,                   // "Отображать вкладку \"Звонки\" на главном экране",
            QPrivMakeAsteriskCalls: 203,                          // "Приваты", "Звонки через интеграцию с Asterisk"
            QPrivGetAsteriskCalls: 204,                           // "Приваты", "Разрешить получать звонки через интеграцию с Asterisk"
            QAllowViewConfMsgReadGotStatus: 205,                  // "Конференции", "Разрешить смотреть статусы доставки/прочтения сообщений"
            QPrivAllowHangUpCalls: 206,                           // "Приваты", "Разрешить завершать звонки"
            QPrivAutoAnswerIncomingCalls: 207,                    // "Приваты", "Автоматически отвечать на входящие звонки"
            QPrivAllowMuteSoundDuringCalls: 208,                  // "Приваты", "Разрешить выключать микрофон во время звонка"
            QPrivAllowDisableCameraDuringCalls: 209,              // "Приваты", "Разрешить выключать камеру во время звонка"
            QPrivAllowRecordMediaCalls: 210,                      // "Приваты", "Разрешить запись звонков"
            QAllowExitTextConfs: 211,                             // "Конференции", "Разрешить покидать текстовые конференции"
            QViewTextConfMembersList: 212,                        // "Конференции", "Показывать список членов конференции"
            QAllowMyChatGuest2Conf: 213,                          // "MyChat Guest", "Отправка приглашений MyChat Guest для входа в конференцию"
            QAllowBrowseTextConfList: 214,                        // "Конференции", "Разрешить просматривать список доступных конференций"

       /*+*/QKanbanAccess: 134,                                   // доступ к Kanban
            QKanbanAddTask: 186,                                  // "Kanban", "Добавление заданий"
            QKanbanProjectAdd: 215,                               // "Kanban", "Проекты", "Создание новых проектов"
            QKanbanProjectEdit: 216,                              // "Kanban", "Проекты", "Редактирование проектов"
            QKanbanProjectClose: 217,                             // "Kanban", "Проекты", "Закрытие проектов"
            QKanbanStageAdd: 218,                                 // "Kanban", "Этапы", "Создание новых этапов"
            QKanbanStageRemove: 219,                              // "Kanban", "Этапы", "Удаление этапов"
            QKanbanStageEdit: 220,                                // "Kanban", "Этапы", "Редактирование этапов"
            QKanbanTaskRemove: 221,                               // "Kanban", "Задачи", "Удаление задач"
            QKanbanTaskEdit: 222,                                 // "Kanban", "Задачи", "Редактирование задач"
            QKanbanTaskMoveBetweenStages: 223,                    // "Kanban", "Задачи", "Перенос задач между этапами"
            QKanbanTaskAccessOnlyPersonal: 224,                   // "Kanban", "Задачи", "Доступ только к своим заданиям"
            QKanbanCommentAdd: 225,                               // "Kanban", "Комментарии", "Добавление комментариев"
            QKanbanCommentRemove: 226,                            // "Kanban", "Комментарии", "Удаление комментариев"

            QUserGroupsCommonView: 227,                           // "Группы пользователей", "Общие группы",  "Доступ к общим группам"
            QUserGroupsPersonalView: 228,                         // "Группы пользователей", "Личные группы", "Доступ к личным группам"
            QUserGroupsPersonalCreate: 229,                       // "Группы пользователей", "Личные группы", "Создание личных групп"
            QUserGroupsPersonalRemove: 230,                       // "Группы пользователей", "Личные группы", "Удаление личных групп"
            QUserGroupsPersonalEdit: 231,                         // "Группы пользователей", "Личные группы", "Редактирование личных групп"
            QTxtConfInsertSourceCode: 232,                        // "Конференции", "Вставка форматированонго исходного кода"
            QPrivInsertSourceCode: 233,                           // "Приваты", "Вставка форматированонго исходного кода"
            QKanbanRemoveSelfComments: 234,                       // "Канбан", "Комментарий", "Удаление своих комментариев"
            QKanbanViewTaskHistory:235,                           // "Канбан", "Комментарий", "Просмотр истории изменения задачи"
            QAdmAccessPersonalGroups: 236,                        // админка, "Пользователи", "Личные группы пользователей"
            QAdmAccessSettingsPlugins: 237,                       // админка, "Настройки", "Плагины"
            QEnableGPSTracking: 238,                              // "Общие", "Отправка текущей геопозиции на сервер"
            QAllowViewUserGeoPosition: 239,                       // "Профиль", "Просмотр информации", "Геопозиция (GPS)"
            QAdmAccessOptionsGPS: 240,                            //
            QFireUsers: 241,                                      // "Администрирование", "Увольнение пользователей"
            QAllowPrintChatText: 242,                             // "Общие", "Печать истории чатов на принтере"
            QAllowSaveChatText: 243,                              // "Общие", "Сохранение истории чатов в файл"
            QAllowRemoteExecScripts: 244,                         // "Integration API", "Разрешить удалённое выполнение скриптов"
            QAllowCustomMenu: 245,                                // "Общие", "Пользовательское меню"
            QAdmAccessToCustomMenu: 246,                          // админка, "Доступ к функциям управления сервером", "Инструменты", "Пользовательские меню"
            QAdmAccessPortTestTool: 247,                          // админка, "Доступ к функциям управления сервером", "Инструменты", "Тест портов"
            QProfileAllowToRemove: 248,                           // "Профиль", "Разрешить удаление своей учётной записи"
            QAllowConnectWindows: 249,                            //
            QAllowConnectLinux: 250,                              //
            QAllowConnectmacOS: 251,                              //
            QAllowConnectWEB: 252,                                //
            QAllowConnectAndroid: 253,                            //
            QAllowConnectiOS: 254,                                //
            QAllowCreateVoiceConf: 255,                           // "Конференции", "Звонки", "Создание голосовых конференций"
            QAllowCreateVideoConf: 256,                           // "Конференции", "Звонки", "Создание видеоконференций"
            QAllowCreateWebinar: 257,                             // "Конференции", "Звонки", "Создание вебинаров"
            QAllowCloseMediaConf: 258,                            // "Конференции", "Звонки", "Закрытие медиа конференций"
            QAllowEnterMediaConf: 259,                            // "Конференции", "Звонки", "Вход в медиа конференции"
            QAllowLeaveMediaConf: 260,                            // "Конференции", "Звонки", "Выход из медиа конференций"
            QAllowRemoveConf: 261,                                // "Конференции", "Удаление конференций"
            QAllowCloseConf: 262,                                 // "Конференции", "Закрытие конференций"
            QAllowRemoveSelfConf: 263,                            // "Конференции", "Удаление своих конференций"
            QAllowCloseSelfConf: 264,                             // "Конференции", "Закрытие своих конференций"
            QAllowEditSelfPrivateMessages: 265,                   // "Приваты", "Редактирование своих сообщений"
            QAllowEditSelfConfMessages: 266,                      // "Конференции", "Редактирование своих сообщений"
        },

        // =====================================================================

        msgType: {
             OLD_SHIT            : 0,
             TEXT                : 1,
            IMAGE               : 2,
            BEEP                : 3,
            OLD_FILE            : 4,  // deprecated
            MARKDOWN            : 5,
            ACTION              : 6,
            LAN_LINK            : 7,
            ANY_LINK            : 8,
            PREVIEW_CUSTOM_LINK : 9,
            INTEGRATION_API     : 10,
            SOURCE_CODE         : 11,
            MEDIA_CALL          : 12,
            IGNORE              : 13,
            SERVICE             : 14,
            CONF_PERSONAL       : 15,
            CONF_ALERT          : 16,
            CONF_CHANGE_TOPIC   : 17,
            CONF_BAN            : 18,
            CONF_TURN_OUT       : 19,
            BROADCAST_NOTIFY    : 20,
            COMMENT             : 21, // simple text, not shown in WEB support (for homemedhelp)
             KANBAN_NOTIFY      : 22, // уведомления с канбан-доски
            FILE                : 23,
             LOCATION           : 24, // географическая позиция
             CONTACT            : 25, // vCard
            AUDIO               : 26, // аудиозапись
            VIDEO               : 27, // видеозапись
            ADM_DELETE_MESSAGE  : 28, // удаление любого сообщения по его индексу
             FORUM_NOTIFY       : 29, // уведомления с форума
            SHOW_RECEIVED_FILES : 'srf', //
            SHOW_UPLOADED_FILES : 'suf', //
            PRIVATE_REDIRECT    : 30, // перенаправление приватного сообщения от одного пользователя - другому

            FORWARD             : 31, // message forward, оригинальное сообщение внутри "Msg", как контейнер
            CLIPBOARD_EXCEL     : 32, // (7.5+), содержимое буфера обмена в текстовом виде таблицы MS Excel
            BARCODE             : 33, // qr или штрих-код
            FTP_PUBLIC          : 34, // 8.0+ общий FTP
            FTP_PERSONAL        : 35, // 8.0+ личный FTP
            CONF_LINK           : 36, // Ссылка на текстовую конференцию, JSON в теле сообщения (поле "Msg")

            CONF_JUST_CREATED   : 38, // Информация о созданной конференции.
            CONF_INVITED        : 40, // Одного или нескольких пользователей пригласили в конференцию.
            YOUTUBE_LINK        : 41, // 8.6+ YouTube-ссылка
            REPLY               : 42, // 8.6+ ответ на сообщение
            REPLY_QUOTE         : 43,
            IMAGE_NEW           : 44, // Изображение
            FILE_NEW            : 45, // файлы
            EXCEL_NEW           : 46, // (8.6+), содержимое буфера обмена в текстовом виде таблицы MS Excel
            FORMATTED_TEXT      : 47, // (8.6+) форматированный текст
            MYCHAT_GUEST        : 48, // (8.8+) приглашение MyChat Guest успешно отправлено
            TEXT_WITH_TAGS      : 50, // (8.10+) текст с тегами [MENTION]123[/MENTION]
            TEXT_MODIFY         : 51, // Редактирование любого текстового сообщения (MSG_TYPE_TEXT) по его индексу.
                                      // "Idx" : 1745, // идентификатор сообщения, которое нужно изменить
                                      // "MsgNew" : "Bla-bla-bla", // текст нового сообщения
                                      // "Modifier": 17 // UIN юзера, который изменил сообщение, необязательный параметр, если месадж редактировал отправитель

            MEDIA_CONF_START    : 52,
            MEDIA_CONF_CLOSE    : 53
        },
        
        msgClientType: {
            WIN32   : 0,
            WEB     : 1,
            MACOS   : 2,
            LINUX   : 3,
            ANDROID : 4
        },

        loginStyle: {
            STANDARD       : '0',
            AD             : '1',
            NO_PASS        : '2',
            AD_TRANSPARENT : '3'
        },

        msgMods: {
            NONE     : 0,
            MODIFIED : 1,
            DELETED  : 2
        },

        confType: {
            STANDARD : 1, // обычная конференция
            PASSWORD : 2, // закрытая паролем
            HIDDEN   : 3  // скрытая
        },

        Errors: {
            WrongPassword       : 14,
            UserNotExist        : 17,
            WrongServerVersion  : 81,
            ConnectionLimit     : 209,
            NetworkError        : 224,
            Disconnected        : 223,
            WebServerStopped    : 321,
            ServerBusy          : 312,

            '---' : '---'
        },
        
        nsInternalErrors: {
            TELEGRAM_SCRIPT_NOT_FOUND      : 1,
            TELEGRAM_SCRIPT_DISABLED       : 2,
            INVALID_SCRIPT_RESULT          : 3,
            SCRIPT_TIMED_OUT               : 4,
            SCRIPT_SYNTAX                  : 5,
            SCRIPT_RUNTIME                 : 6,
            SCRIPT_NO_ONREQUESTBYREST      : 7,
            SCRIPT_INVALID_ONREQUESTBYREST : 8
        },

        whereType: {
            PRIVATE    : 1,
            CONFERENCE : 2,
            FORUM      : 3,
            KANBAN     : 4,
            BBS        : 5,
            BROADCAST  : 6,
        },

        ntfType: {
            ANDROID_SETUP             : 100, // просьба установить Android-приложение
            COMMON                    : 101, // встроенное уведомление, генерируется локально
            LINUXMACWEB               : 102, // встроенное уведомление, linux/mac/web
            UTP                       : 107, // встроенное уведомление, платные услуги
            GAME                      : 108, // встроенное уведомление, робот-викторина и т.п. развлекательные штуки
            IOS_SETUP                 : 109, // просьба установить iOS-приложение
            NEED_SUBSCRIPTION_2MONTHS : 110, // осталось два месяца до конца действия подписки
            NEED_SUBSCRIPTION_1MONTH  : 111, // остался месяц до конца действия подписки
            NEED_SUBSCRIPTION_2WEEKS  : 112, // осталось две недели до конца действия подписки
            SUBSCRIPTION_EXPIRED      : 113, // подписка просрочена
            FREE_VER_EXPIRED          : 114, // срок использования бесплатной версии закончился
        },

        updateConsoleCMD : "--updateClient",
        normalStart      : "--start",

        _last_: '_last_' // просто, шоб було
        // ==========================================
});

window.JS_SHA1_NO_COMMON_JS = true;

var mcConst = {
    Title : "MyChat",
    LMSG  : [],
    _CMD_ : window.hasOwnProperty('getCMD') ? getCMD() : {},
    countHistoryMessagesLoad: 50,
    minChatFrameWidth : 300,
    currentDownloadFolder: "",

    ElisaUIN: 0,
    
    isWindowFocused: true,

    mainWindowSate: {
        visible : true,
        minimize: false,
        focus   : true
    },

    socketIO : null,

    io_transport: ["polling"], // , "websocket"
    
    avatars: {
        SMALL : 19,
        MEDIUM: 23,
        LARGE : 32
    },
    // ClientSettings: {},

    clientType : "websupport",

    changeProtocol: location.protocol,

    nodePathes: {
        images: "sendimages",
        files : "files"
    },

    imageSize: {
        small : {
            x: 120,
            y: 120,
            xy: '120x120'
        },
        medium: {
            x: 150,
            y: 150,
            xy: '150x150'
        },
        big   : {
            x: 200,
            y: 200,
            xy: '200x200'
        },
        large   : {
            x: 400,
            y: 400,
            xy: '400x400'
        },
        "0"   : {
            x: 120,
            y: 120,
            xy: '120x120'
        },
        "1"   : {
            x: 150,
            y: 150,
            xy: '150x150'
        },
        "2"   : {
            x: 200,
            y: 200,
            xy: '200x200'
        },
        "3"   : {
            x: 400,
            y: 400,
            xy: '400x400'
        }
    },

    loadGif : "<img src='source/images/loading.gif'>",

    reconnectTime : 5000,

    lockPrevHotKeys: true,

    maxAvailableDate: '01.01.3000.00.00.00',
    neverDate: '01.01.1900.00.00.00',

    storageOpts: {
        //OPENUSERS : 'openUsers-', // not used now
        DIALOG_LIST     : 'dialog_list-',
        SMALLUSESRINFO  : 'smallusersinfo-',
        SEQUENCE        : 'sequence-',
        STATUSICO       : 'statusU-',
        DIALOGOPT       : 'dialogOpt-',
        WRAPOPTS        : 'wrapOpts-',
        INPUTOPTS       : 'inputOpts-',
        HISTORYDLG      : 'historyDialogs-',
        CURRENTITM      : 'currentItem-',
        MAIN_OPTIONS    : 'main_options-',

        // -------

        cUserOpt : 'cUserOpt-'
    },

    Lang : "ru",

    contactsCaches: {
        PublicContacts   : 0, // общий список контактов, если 0 - то кеша нет
        PersonalContacts : 0  // личный список контактов, если 0 - кеша нет
    },

    confsGrants : {},
    offlineFiles: {},

    containers: {
        left   : "wrapperLeft",
        center : "wrapperCenter",
        right  : "wrapperRight"
    },

    lockInterface : 'lockInterface',

    mirrorStreamName : 'mirrorStreamName',
    videoStreamName  : 'videoStreamName',
    audioStreamName  : 'audioStreamName',
    videoContainer   : 'videoFrameContainer',

    storageFields: {
        lang     : "supLang",
        Login    : "supLogin",
        Email    : "supEmail",
        Pwd      : "supPwd",
        ServPwd  : "supServPwd",
        Rm       : "supRm",
        AutoRld  : "supNoAutoConnect",
        chatWith : "supChatWith"
    },

    defaultUserFields: "DISPLAYNAME,SEX,UIN,EMAIL,WORK_PHONE,WORK_DIVDEPT,HOME_BIRTHDAY,WORK_POSITION,WORK_COMPANYNAME,CLIENTVERSION,ROLE",

    userFields : 'REMOTECLIENTTYPE,REMOTEINTERFACESLIST,LASTACCESS,LASTSPEAKTIME,STATE,TAG,HARDWAREID,DOMAIN,' +
                 'DOMAINREGTIME,DOMAINCHANGETIME,VER,UTC,REPUTATION,NICK,DISPLAYNAME,EMAIL,GROUPRIGHTS,COMPNETNAME,' +
                 'IP,MAC,UIN,ACTIVE,REGISTERED,WORKDEPT,TEAMLEAD', //

    dataModels : {
        Login            : 'Login',
        Main             : 'Main',
        ChatWrapper      : 'ChatWrapper',
        ChatFrame        : 'ChatFrame',
        ConfUserList     : 'ConfUserList',
        UserProfile      : 'UserProfile',
        Files            : 'Files',
        CommonContacts   : 'CommonContacts',
        PersonalContacts : 'PersonalContacts',
        Dialogs          : 'Dialogs',
        PrivateConfs     : 'PrivateConfs',
        PrivateInfo      : 'PrivateInfo',
        ServerManager    : 'ServerManager',
        ConfManager      : 'ConfManager',
        MainMenu         : 'MainMenu',
        Kanban           : 'Kanban',
        Admin            : 'Admin',
        BBS              : 'BBS',
        CommonFiles      : 'CommonFiles',
        HistoryDialogs   : 'HistoryDialogs',
        ViewLogs         : 'ViewLogs',
        ReceiveFiles     : 'ReceiveFiles',
        Settings         : 'Settings',
        Broadcast        : 'Broadcast',
        SendBroadcast    : 'SendBroadcast',
        HistoryBroadcast : 'HistoryBroadcast',
        Update           : 'Update',
        Forum            : 'Forum',
        Search           : 'Search',
        CreateConf       : 'CreateConf',
        Important        : 'Important',

        History          : 'History'
    },

    sexIcon : {
        0: "<span class='fa fa-user'></span>",
        1: "<span class='fa fa-male'></span>",
        2: "<span class='fa fa-female'></span>"
    },

    imagesPath : {
        noFilter   : 'source/images/nofilter20.png',
        loading    : 'source/images/loading.gif',
        all        : 'source/images/',
        flags      : 'source/images/flags/',
        loadImage  : 'source/images/noimage.png',
        nofile     : 'source/images/nofile.gif',
        noImage    : 'source/images/noimage.png'
    },

    pathAliases : {
        AliasAdmin    : "admin", // папка-псевдоним, путь к админке
        AliasAPI      : "API", // папка-псевдоним, путь к Integration API
        AliasChat     : "chat", // папка-псевдоним, путь к WEB-чату
        AliasFiles    : "files", // папка-псевдоним, путь к файлам, залитым на сервер
        AliasForum    : "forum", // папка-псевдоним, путь ко встроенному форуму
        AliasKanban   : "kanban", // папка-псевдоним, путь к канбан-доске
        
        LobbyEnable   : true, // включен доступ к списку сервисов MyChat
        ForumEnable   : true, // включить доступ ко встроенному форуму
        KanbanEnable  : true, // включить доступ к канбан-доске
        ChatEnable    : true, // включить доступ к WEB-чату
        AdminEnable   : true // включить доступ к WEB-админке
    },

    whereFiles: {
        private: 1,
        conf   : 2,
        forum  : 3,
        kanban : 4,
        bbs    : 5,
        broadc : 6
    },

    states: {
        offline    : -1,
        online     : 0,
        away       : 1,
        dnd        : 2,
        webOnline  : 3
    },

    currentState: 0,

    LicenseInfo : {
        "ServerRegisteredBy"      : "Network Software Solutions", // на кого зарегистрирован сервер
        "IsFree"                  : false,                        // (5.7+) если сервер бесплатный, то true, иначе - false
        "IsMyChatGuestRegistered" : false                          // зарегистрирован ли сервис MyChat Guest
    },

    customLogin : false,

    lockWindowHeight : 30,

    ApplyPanelShowed : false,

    errMessageExpire : 5000,

    guestGroupId : 7,

    userFoto : {
        w : 135,
        h : 157
    },

    CRLF : '\r\n',
    CR   : '\r',
    LF   : '\n',

    ignores: {
        all          : "all", //- игнорировать всё
        confs        : "confs", //- сообщения в конференциях
        confpersonal : "confpersonal", //- личные сообщения в конференциях
        confalert    : "confalert", //- личные важные сообщения в конференциях
        private      : "private", //- приватные сообщения
        privatebeep  : "privatebeep", //- звуковые сигналы в привате
        info         : "info", //- запросы персональной информации о пользователе
        files        : "files", //- отправка файлов
        plugins      : "plugins" //- запросы плагинов
    },

    MyRightsSet : [
      /*
        + {1}   QLogin, "811":"Подключаться к MyChat серверу",
        + {2}   QBBSPost, "786":"Создание новых сообщений на доске",
        + {3}   QBBSView, "785":"Просмотр доски объявлений",
        + {4}   QTxtChCreate, "797":"Создание текстовых каналов",
        + {5}   QTxtChSay, "844":"Отправка сообщений в текстовые каналы",
        + {6}   QTxtChJoin, "796":"Вход в существующие текстовые каналы",
        + {7}   QPrivateOpen, "831":"Открытие новых приватов",
        + {8}   QPrivatesGetMessages, "832":"Получение приватных сообщений от других пользователей",
        + {9}   QUserDetailsChange, "837":"Изменение персонального профиля",
        + {10}  QFilesSend, "841":"Разрешить отправлять файлы другим пользователям",
        + {11}  QFilesReceive, "842":"Разрешить принимать файлы от других пользователей",
        + {12}  QOperatorBan, "821":"Право наказывать пользователей (ban)",
        + {13}  QOperatorTurnOutFromChannel, "820":"Право изгнания пользователей из текстовых каналов (kick)",
        + {14}  QKill,
        + {15}  QBlockIP,
        + {16}  QBlockMAC,
        + {17}  QOperatorTxtChTopicSet, "818":"Изменение темы текстовых каналов",
        + {18}  QFTPPublic, "840":"Доступ к общей папке на файловом сервере",
        + {19}  QFTPPersonal, "839":"Доступ к личной папке на файловом сервере",
        + {20}  QRemoteAdm,
        + {21}  QOperatorClearChannelText, "819":"Очистка текстовых каналов у всех пользователей",
        + {22}  QContactsListChange, "799":"Изменение персональной панели контактов",
        + {23}  QBroadcasts, "823":"Отправка оповещения другим пользователям",
        + {24}  QAllowPersonalMsg, "794":"Отправка персональных сообщений в каналах",
        + {25}  QAllowAlertMsg, "795":"Отправка важных персональных сообщений в каналах",
        + {26}  QAllowSysInfoView, "815":"Просмотр сетевого имени компьютера и IP-адреса пользователя",
        + {27}  QSendImages, "792":"Вставка изображений",
        + {28}  QAllowIgnores, "805":"Игнорирование других пользователей",
        + {29}  QAllowInvites, "810":"Отправка приглашений в каналы другим пользователям",
        + {30}  QImagesPrivates, "830":"Вставка изображений",
        + {31}  QShowCommonContactsList, "800":"Показывать общий список контактов",
        + {32}  QHalt,
        + {33}  QTxtChRename,
        + {34}  QTxtChDelete,
        + {35}  QForciblyInvites,
        + {36}  QBlockUIN,
        + {37}  QAllowPlugins, "827":"Использование плагинов",
        + {38}  QAllowOptionsChange, "806":"Изменение настроек клиентского приложения",
        + {39}  QAllowMyFavoriteChannels, "793":"Доступ к ''Моим любимым каналам''",
        + {40}  QAllowCloseProgram,
        + {41}  QAllowContactsListsTotal,                         // разрешить или запретить полностью доступ к панели контактов
        + {42}  QAllowAccountsManager,                            // разрешить пользоваться менеджером учётных записей
        + {43}  QWEBAllowViewLogsSelfPrivates,                    // просмотр своих разговоров в приватах
        + {44}  QWEBAllowViewLogsAllChannels,                     // просмотр всех разговоров в каналах
        + {45}  QWEBAllowViewLogsAllPrivates,                     // просмотр всех разговоров в приватах
        + {46}  QAllowFindUsersTool,                              // разрешить поиск пользователей
        + {47}  QAllowActions,                                    // разрешить вставку в текст Action-ов
        + {48}  QAllowClientLogPrivatesAndChannels,               // разрешить или запретить клиенту писать логи локально, у себя на компьютере
        + {49}  QMessengerStyle,                                  // Messenger-style отображения майчат клиента
        + {50}  QAllowOpenComputer,                               // показывать или нет в меню опцию "Открыть компьютер" по щелчку на имени юзера
        + {51}  QExceptionAllowRecievePrivates,                   // Приём приватных сообщений от пользователей, которым запрещена отправка приватных сообщений
        + {52}  QAllowViewRemoteUserMACAddress,                   // разрешить смотреть MAC адрес удалённого юзера
        + {53}  QAllowViewRemoteUserActiveProcess,                // разрешить смотреть активный процесс удалённого юзера
        + {54}  QWEBAllowViewFTPLog,                              // Просмотр FTP протоколов
        + {55}  QWEBAllowViewSystemLog,                           // Просмотр системных протоколов
        + {56}  QEnableTaskWorkersJobTimeControlSystem,           // контроль рабочего времени
        + {57}  QWEBSupportAllowAccess,                           // доступ к WEB-интерфейсу веб-суппорта
        + {58}  QWEBAllowAdmin,                                   // администрирование сервере через WEB
        + {59}  QAllowProgramBlockTool,                           // Блокирование работающей программы паролем
        + {60}  QAllowNetworkStatusChange,                        // Изменение сетевого статуса
        + {61}  QEnableAllowedClientsPluginsList,                 // Активировать список разрешённых клиентских плагинов
        + {62}: QEnableBlockedCLientsPluginsList,                 // Активировать список запрещённых клиенских плагино,
        + {63}  QEnablePluginsManagement,                         // Разрешить управление плагинами на клиенте
        + {64}  QAllowImagesInBroadcasts,                         // вставка картинок в оповещениях
        + {65}  QEnableFontLayoutsInMessages,                     // использование шрифтовой разметки в отправляемых сообщениях
          {66}  QAllowCreateHiddenChannels,                       // - (это ещё не внесено в базу данных) разрешить создавать скрытые каналы
        + {67}  QEnableTransferFilesOnlyViaServer,                // передавать файлы только через сервер
        + {68}  QEnableImmunityFromBans,                          // иммунитет от банов
        + {69}  QEnableAdvertBlock                                // показывать или нет рекламный блок
        + {70}  QBlockPrivatesExceptPersonalContacts,             // Запретить приваты со всеми, кроме входящих в личный список контактов
        + {71}  QBlockPrivatesExceptCommonContacts,               // Запретить приваты со всеми, кроме входящих в общий список контактов
        + {72}  QEnableImmunityFromIgnores,                       // Иммунитет от игноров
        + {73}  QIntegrationAPIEnableSendMsgFromAnyUser,          // Разрешить отправку сообщений от имени этих пользователей
        + {74}  QEnableVoiceCalls,                                // Голосовые звонки
        + {75}  QEnableVideoCalls,                                // Видеозвонки
        + {76}  QAllowGetVoiceCalls,                              // Разрешить получать голосовые звонки
        + {77}  QAllowGetVideoCalls,                              // Разрешить получать видеозвонки
        + {78}  QAllowAddNewPlugins,                              // Добавление новых плагинов
        + {79}  QAllowDeletePlugins,                              // Удаление плагинов
        + {80}  QAllowChangeAccountPassword,                      // Изменение пароля своей учётной записи
        + {81}  QAllowAccessForeignersToPersonalFiles,            // Разрешить доступ к личным файлам для других пользователей ("только чтение")
        + {82}  QAllowPublishLinkToPersonalFiles,                 // Разрешить публиковать ссылку на "Мои файлы на сервере"
        + {83}  QFTPPublicWrite,                                  // разрешить писать в общую папку
        + {84}  QFTPPersonalWrite                                 // разрешить писать в личную папку
        + {85}  QAllowSendInvites                                 // разрешить отправлять приглашения в MyChat другим людям
        + {86}  QWEBAllowViewNodeJSLogs,                          // разрешить просмотр логов NodeJS сервера
        + {87}  QWEBAllowViewAuditLogs                            // разрешить просмотр логов аудита
      */
    ],

    LoginInfo : {
        login   : "",
        pwd     : "",
        servPwd : "",
        rm      : ""
    },

    Auth      : '',
    PWD       : '',
    servPass  : '',

    BrowserID : '',
    SessionID : '',

    LoggedIn  : false,
    nowLogin  : false,

    terminator       : '\u2022',
    terminator2      : '\u0002',
    terminator3      : '\u0003',
    newLine          : '\u000b',

    myChatLinkTitle  : "?specialMyChatLinkTitle=",

    ftpPersonal      : "ftp://personal_",
    ftpPublic        : "ftp://public",

    PingTimer        : null,
    PingInterval     : 10000,

    minEnterTextHeight: 75,

    mcPara           : "L$",
    mcParaRight      : "R#$",
    mcParaCenter     : "C#$",

    UserInfo   : {                     // информация о залогиненом юзере
        MaxUploadFileSize: 0,
        PvDlgIdx        : 0,           // (7.2+) число, автоинкремент, при любом изменении диалогов или сообщений в них увеличивается на единицу. Если 0 - приватных диалогов ещё нет
        ChangeID        : -1,          // (7.5.1+) числовой идентификатор изменений. Если данные пользователя менялись или у пользователя есть отложенные технические сообщения - этот ID будет увеличен
        UIN             : 0,           // UIN пользователя, который залогинен в системе
        Nick            : "__Anonymous__", // ник пользователя, который сообщил ему сервер
        Sex             : 1,           // пол пользователя
        Avatar          : 0,           // номер аватара пользователя
        State           : 0,
        OptionsPresetID : 0,
        Templates       : {1:"",2:"",3:"",4:"",5:"",6:"",7:"",8:"",9:""},
        OptionsPresetCRC32: 0,
        DisplayName     : "__Anonymous__",
        LoginStyle      : "login",     // тип логина: "login" - обычный логин, "domain" - доменная авторизация
        AutoAwayTime    : 15,          // время в минутах, после которого будет считаться, что пользователь неактивен
                                       // за компьютером, если он не нажимал ничего на клавиатуре и не шевелил мышкой
        ServerSignature : "",          // специальная сигнатура сервера MyChat, нужна для системы статистики
        HWID            : "",          // серийник сервера
        Domain          : ""           // если доменная авторизация, "LoginStyle" = "domain", то в этом параметре
                                       // передаётся ещё и имя домена, с которого проходит аторизация.
                                       // Если доменной авторизации нет - этот параметр не передаётся
    },

    ErrorText   : {},
    SrvErrorText: {},

    CanDisplayNotificator : false,
    NotificatorObject     : null,

    ExtendedMode           : true,
    CanChangToExtendedMode : true,
    WidthForExtendedMode   : 800,

    oneDay    : 1000*60*60*24,
    navWidth  : 270,

    isShowed  : null,
    nextShow  : '',

    scrResize : null,
    navResize : null,

    noNotify  : false,
    notify    : null,
    idNotify  : 0,

    ServerInfo: {
        Host: '127.0.0.1',
        Port: 2004,
        ID  : null
    },

    keyCodes  : {
        // Alphabet
        a:65, b:66, c:67, d:68, e:69,
        f:70, g:71, h:72, i:73, j:74,
        k:75, l:76, m:77, n:78, o:79,
        p:80, q:81, r:82, s:83, t:84,
        u:85, v:86, w:87, x:88, y:89,
        z:90,

        // Numbers
        n0:48, n1:49, n2:50, n3:51, n4:52,
        n5:53, n6:54, n7:55, n8:56, n9:57,

        // Controls
        tab:  9, enter :13,  shift :16, backspace:8,
        ctrl:17, alt   :18,  esc   :27, space    :32,
        menu:93, pause :19,  cmd   :91, insert   :45,
        home:36, pageup:33,'delete':46, end      :35,
        pagedown:34,

        // F*
        f1:112, f2:113, f3:114, f4 :115, f5 :116, f6 :117,
        f7:118, f8:119, f9:120, f10:121, f11:122, f12:123,

        // numpad
        np0: 96, np1: 97, np2: 98, np3: 99, np4:100,
        np5:101, np6:102, np7:103, np8:104, np9:105,

        npslash:11,npstar:106,nphyphen:109,npplus:107,npdot:110,

        // Lock
        capslock:20, numlock:144, scrolllock:145,

        // Symbols
        equals: 61, hyphen   :109, coma  :188, dot:190,
        gravis:192, backslash:220, sbopen:219, sbclose:221,
        slash :191, semicolon: 59, apostrophe : 222,

        // Arrows
        aleft:37, aup:  38, aright:39, adown:40,

        PrintSymbols :
            [32, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 65,
                66, 67, 68, 69, 70, 71, 72, 73, 74, 75, 76,
                77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87,
                88, 89, 90, 96, 97, 98, 99, 100,101,102,103,
                105,106,107,109,110,111,186,187,188,189,190,
                104,191,192,219,220,221,222],

        EditSymbols  : [8,  32, 46],

        Digits :
            [48, 49, 50, 51,  52, 53, 54, 55, 56, 57,
                96, 97, 98, 99, 100,101,102,103,104,105],

        ControlFunc	 :
            [ 33, 34, 35, 36, 37, 38, 39, 40,112,113,
                114,115,116,117,118,119,120,121,122,123],

        ControlsKeys : [36, 33, 35, 34, 37, 38, 39, 40],

        PrintAndEdit : function(){
            return mcConst.keyCodes.PrintSymbols.concat(mcConst.keyCodes.EditSymbols);
        }
    },

    KanbanEvents: {
        TASK_ADDED             : 1,
        TASK_PERFORMER_CHANGED : 2,
        COMMENT_ADDED          : 3,
        TASK_MOVED             : 4,
        TASK_CHANGED           : 5,
        TASK_DELETED           : 6,
        PROJECT_CLOSED         : 7
    },

    historyTypes: {
        count: 1,
        range: 2,
        list : 3
    },

    FTP: {
        CommonUser: 'mcuser',
        CommonPWD : 'mychat'
    },

    SmileysSet: [],

    ClientSettings : {
        "NetworkReconnectTime" : "10",
        "GeneralShowSendButton" : 1,
        "GeneralConfirmExit" : 1,
        "GeneralMainWindow" : 0,
        "GeneralCtrlEnterSend" : 0,
        "GeneralSpellCheck" : 1,
        "GeneralDoubleClickPagesClose" : 1,
        "SpecialMessagesFontSize" : 1,
        "SoundsAllSoundsOff" : 0,
        "EventsPopupOnNewBBS" : 1,
        "EventsPopupOnChannelMessage" : 0,
        "EventsPopupOnPrivateMessage" : 0,
        "EventsTimeStamp" : 1,
        "EventsShowPopupTrayWindow" : 1,
        "EventsTrayBBSMsg" : 1,
        "EventsTrayChMsg" : 1,
        "EventsTrayPvMsg" : 1,
        "EventsTrayBlinkOnTaskBar" : 1,
        "EventsTrayDontHide" : 0,
        "PreviewDropFile": 0,

        "SoundsSndNewMsg" : 1,
        "SoundsSndBadWord" : 1,
        "SoundsSndLeave" : 1,
        "SoundsSndJoin" : 1,
        "SoundsSndChatType" : 1,
        "SoundsSndChatBS" : 1,
        "SoundsSndStatus" : 1,
        "SoundsSndError" : 1,
        "SoundsSndPrivate" : 1,
        "SoundsSndChatRet" : 1,
        "SoundsSndSignal" : 1,
        "SoundsSndChat" : 1,
        "SoundsSndBroadcast" : 1,
        "SoundsSndFilesExchangeRequest" : 1,
        "SoundsSndFilesExchangeDone" : 1,
        "SoundsSndScreenShot" : 1,
        "SoundsSndMediaBusy" : 1,
        "SoundsSndMediaCall" : 1,
        "SoundsSndMediaCallReject" : 1,
        "SoundsSndMediaEndCall" : 1,
        "SoundsSndMediaIncomingCall" : 1,

        "SoundsSndNewMsgFile" : "newmsg.mp3",
        "SoundsSndBadWordFile" : "badword.mp3",
        "SoundsSndLeaveFile" : "leave.mp3",
        "SoundsSndJoinFile" : "join.mp3",
        "SoundsSndChatTypeFile" : "chattype.mp3",
        "SoundsSndChatBSFile" : "chatbs.mp3",
        "SoundsSndStatusFile" : "status.mp3",
        "SoundsSndErrorFile" : "error.mp3",
        "SoundsSndPrivateFile" : "private.mp3",
        "SoundsSndChatRetFile" : "chatret.mp3",
        "SoundsSndSignalFile" : "signal.mp3",
        "SoundsSndChatFile" : "chat.mp3",
        "SoundsSndBroadcastFile" : "broadcast.mp3",
        "SoundsSndScreenShotFile" : "screenshot.mp3",
        "SoundsSndFilesExchangeRequestFile" : "filesrequest.mp3",
        "SoundsSndFilesExchangeDoneFile" : "filesdone.mp3",
        "SoundsSndMediaBusyFile" : "mediabusy.mp3",
        "SoundsSndMediaCallFile" : "mediacall.mp3",
        "SoundsSndMediaCallRejectFile" : "mediacallreject.mp3",
        "SoundsSndMediaEndCallFile" : "mediaendcall.mp3",
        "SoundsSndMediaIncomingCallFile" : "mediaincomingcall.mp3",

        // ------------------------------------------

        "NetworkSecured" : 0,
        "NetworkProxyType" : 0,
        "NetworkProxyHost" : "",
        "NetworkProxyPort" : 1080,
        "NetworkProxyAuth" : 0,
        "NetworkProxyUserName" : "",
        "NetworkProxyUserPass" : "",

        "GeneralWindowsStart" : 1,
        "GeneralStartPassword" : 0,
        "GeneralDailyUpdatesCheck" : 1,
        "GeneralOpenPrivateByOneClick" : 1,
        "GeneralFadeWindows" : 1,

        //"GeneralShowUsersCounterOnTheTabs" : 1,
        //"GeneralShowMainToolsPanel" : 1,


        "GeneralTipOfTheDayNum" : 3,
        "GeneralShowTipOfTheDay" : 0,
        //"GeneralDisableAvatars" : 1,
        "GeneralQuickMsgAutoSend" : 0,

        "GeneralAutoHideMainWindow" : 0,
        "GeneralRememberOpenContactGroups" : "",

        "GeneralSeparateMyMessages": 0,
        "GeneralOnlyOnlineInCommonContacts" : 0,

        // "EventsDateTimeStampFormat" : "[hh:nn:ss]",
        "EventsTrayWindowDuration" : 5,
        "EventsOpenPrivateOnPersonalOrAlert" : 0,

        "HotKeysHotKeyMyChat" : "<Win>+F12",
        "HotKeysHotKeyMyChatScreenShot" : "<Win>+F9",
        "HotKeysUseGlobalHotKeys" : 1,
        "HotKeysEscHide" : 1,

        "SmileysSmileysSet" : "classic",

        // "SysEventsShowSysEvents" : 1,
        // "SysEventsEnterTxtxCh" : 1,
        // "SysEventsLeaveTxtxCh" : 1,
        // "SysEventsQuitChat" : 1,
        // "SysEventsChangeTheme" : 1,
        // "SysEventsBanUser" : 1,
        // "SysEventsTurnOut" : 1,
        // "SysEventsIgnore" : 1,
        // "SysEventsConnectionLost" : 1,
        // "SysEventsConnectionRestored" : 1,
        "SysEventsDisableEmotions" : 0,
        // "SysEventsEnableSmileysAnimation" : 1,
        // "SysEventsAutoreplaceSmileys" : 1,
        "SysEventsUseMessagesFontLayouts" : 1,
        "SysEventsLoadHistoryType" : 1,
        "SysEventsHistory_1_Num" : 50,
        // "SysEventsAutoFillBroadcastUsersList" : 1,
        "SysEventsShowImagesInChat" : 1,
        "SysEventsImagesThumbsSize" : 2,

        "LogsServer" : 1,
        "LogsTxtChannels" : 1,
        "LogsPrivates" : 1,
        "LogsFTP" : 1,

        "SecurityOptionsPassword" : "",
        "LanguagesLanguage" : "russian.ini",

        // "SendFilesInputFilesDir" : "%s\\Documents\\MyChat\\",
        "SendFilesInputFilesDir" : "",
        "SendFilesBasePort" : "10000",
        "SendFilesDataPort" : "10001",
        "SendFilesBufferSize" : "3",
        "SendFilesSayYes" : 0,
        "SendFilesRememberSelectInputFilesFolder" : 1,
        "SendFilesRandomPorts" : 1,
        "SendFilesRandomPortStart" : "10000",
        "SendFilesRandomPortEnd" : "20000",
        "SendFilesRenameRecievedDuplicates" : "",

        "Colorscolor_ch_pv_back" : "13758972",
        "Colorscolor_inp_back" : "13758972",
        "Colorscolor_userslist_back" : "13758972",
        "Colorscolor_userslist_text" : 0,
        "Colorscolor_own_nick_text" : "255",
        "Colorscolor_active_link" : "255",
        "Colorscolor_visited_link" : "8388736",
        "Colorscolor_timestamp" : "10526880",
        "Colorscolor_hello_nick" : "13209",
        "Colorscolor_users_nick_text" : "16711680",

        "UpdateUseAccountForUpdates" : 0,
        "UpdateUpdateLogin" : "",
        "UpdateUpdatePassword" : "",
        "UpdateUpdateDomain" : "",
        "UpdateForceUpdateFromMyChatServerInActiveDirectory" : 0,


        "SpecialToolsPanelType" : 1,
        //"SpecialPagesPanel" : "3",

        "AdditionalPrivateInfoFields" : "DISPLAYNAME,SEX,UIN,EMAIL,WORK_PHONE,WORK_DIVDEPT,HOME_BIRTHDAY,WORK_POSITION,WORK_COMPANYNAME,CLIENTVERSION,ROLE",
        "AdditionalProgramCaption" : "%program% %ver% - %nickname% %company% (%state%)",
        "AdditionalSystemTrayText" : "MyChat Client",

        "InterfaceTrayIconListNumber" : 0,
        "InterfaceCommonBackground" : "",
        "InterfaceBackgroundPlacement" : "",

        "MediaNetworkTransportTCP" : 1,
        "MediaNetworkTransportUDP" : 1,
        "IceTransportPolicy" : "all",

        "LoadHistoryToConf" : 1,
        "LoadHistoryToPrivate" : 1
    }
};
(function () {
    var /*
 * Rusha, a JavaScript implementation of the Secure Hash Algorithm, SHA-1,
 * as defined in FIPS PUB 180-1, tuned for high performance with large inputs.
 * (http://github.com/srijs/rusha)
 *
 * Inspired by Paul Johnstons implementation (http://pajhome.org.uk/crypt/md5).
 *
 * Copyright (c) 2013 Sam Rijs (http://awesam.de).
 * Released under the terms of the MIT license as follows:
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 * IN THE SOFTWARE.
 */
    util = {
        getDataType: function (data) {
            if (typeof data === 'string') {
                return 'string';
            }
            if (data instanceof Array) {
                return 'array';
            }
            if (typeof global !== 'undefined' && global.Buffer && global.Buffer.isBuffer(data)) {
                return 'buffer';
            }
            if (data instanceof ArrayBuffer) {
                return 'arraybuffer';
            }
            if (data.buffer instanceof ArrayBuffer) {
                return 'view';
            }
            if (data instanceof Blob) {
                return 'blob';
            }
            throw new Error('Unsupported data type.');
        }
    };
    function Rusha(chunkSize) {
        'use strict';
        var // Private object structure.
        self$2 = { fill: 0 };
        var // Calculate the length of buffer that the sha1 routine uses
        // including the padding.
        padlen = function (len) {
            for (len += 9; len % 64 > 0; len += 1);
            return len;
        };
        var padZeroes = function (bin, len) {
            var h8 = new Uint8Array(bin.buffer);
            var om = len % 4, align = len - om;
            switch (om) {
            case 0:
                h8[align + 3] = 0;
            case 1:
                h8[align + 2] = 0;
            case 2:
                h8[align + 1] = 0;
            case 3:
                h8[align + 0] = 0;
            }
            for (var i$2 = (len >> 2) + 1; i$2 < bin.length; i$2++)
                bin[i$2] = 0;
        };
        var padData = function (bin, chunkLen, msgLen) {
            bin[chunkLen >> 2] |= 128 << 24 - (chunkLen % 4 << 3);
            // To support msgLen >= 2 GiB, use a float division when computing the
            // high 32-bits of the big-endian message length in bits.
            bin[((chunkLen >> 2) + 2 & ~15) + 14] = msgLen / (1 << 29) | 0;
            bin[((chunkLen >> 2) + 2 & ~15) + 15] = msgLen << 3;
        };
        var // Convert a binary string and write it to the heap.
        // A binary string is expected to only contain char codes < 256.
        convStr = function (H8, H32, start, len, off) {
            var str = this, i$2, om = off % 4, lm = (len + om) % 4, j = len - lm;
            switch (om) {
            case 0:
                H8[off] = str.charCodeAt(start + 3);
            case 1:
                H8[off + 1 - (om << 1) | 0] = str.charCodeAt(start + 2);
            case 2:
                H8[off + 2 - (om << 1) | 0] = str.charCodeAt(start + 1);
            case 3:
                H8[off + 3 - (om << 1) | 0] = str.charCodeAt(start);
            }
            if (len < lm + om) {
                return;
            }
            for (i$2 = 4 - om; i$2 < j; i$2 = i$2 + 4 | 0) {
                H32[off + i$2 >> 2] = str.charCodeAt(start + i$2) << 24 | str.charCodeAt(start + i$2 + 1) << 16 | str.charCodeAt(start + i$2 + 2) << 8 | str.charCodeAt(start + i$2 + 3);
            }
            switch (lm) {
            case 3:
                H8[off + j + 1 | 0] = str.charCodeAt(start + j + 2);
            case 2:
                H8[off + j + 2 | 0] = str.charCodeAt(start + j + 1);
            case 1:
                H8[off + j + 3 | 0] = str.charCodeAt(start + j);
            }
        };
        var // Convert a buffer or array and write it to the heap.
        // The buffer or array is expected to only contain elements < 256.
        convBuf = function (H8, H32, start, len, off) {
            var buf = this, i$2, om = off % 4, lm = (len + om) % 4, j = len - lm;
            switch (om) {
            case 0:
                H8[off] = buf[start + 3];
            case 1:
                H8[off + 1 - (om << 1) | 0] = buf[start + 2];
            case 2:
                H8[off + 2 - (om << 1) | 0] = buf[start + 1];
            case 3:
                H8[off + 3 - (om << 1) | 0] = buf[start];
            }
            if (len < lm + om) {
                return;
            }
            for (i$2 = 4 - om; i$2 < j; i$2 = i$2 + 4 | 0) {
                H32[off + i$2 >> 2 | 0] = buf[start + i$2] << 24 | buf[start + i$2 + 1] << 16 | buf[start + i$2 + 2] << 8 | buf[start + i$2 + 3];
            }
            switch (lm) {
            case 3:
                H8[off + j + 1 | 0] = buf[start + j + 2];
            case 2:
                H8[off + j + 2 | 0] = buf[start + j + 1];
            case 1:
                H8[off + j + 3 | 0] = buf[start + j];
            }
        };
        var convBlob = function (H8, H32, start, len, off) {
            var blob = this, i$2, om = off % 4, lm = (len + om) % 4, j = len - lm;
            var buf = new Uint8Array(reader.readAsArrayBuffer(blob.slice(start, start + len)));
            switch (om) {
            case 0:
                H8[off] = buf[3];
            case 1:
                H8[off + 1 - (om << 1) | 0] = buf[2];
            case 2:
                H8[off + 2 - (om << 1) | 0] = buf[1];
            case 3:
                H8[off + 3 - (om << 1) | 0] = buf[0];
            }
            if (len < lm + om) {
                return;
            }
            for (i$2 = 4 - om; i$2 < j; i$2 = i$2 + 4 | 0) {
                H32[off + i$2 >> 2 | 0] = buf[i$2] << 24 | buf[i$2 + 1] << 16 | buf[i$2 + 2] << 8 | buf[i$2 + 3];
            }
            switch (lm) {
            case 3:
                H8[off + j + 1 | 0] = buf[j + 2];
            case 2:
                H8[off + j + 2 | 0] = buf[j + 1];
            case 1:
                H8[off + j + 3 | 0] = buf[j];
            }
        };
        var convFn = function (data) {
            switch (util.getDataType(data)) {
            case 'string':
                return convStr.bind(data);
            case 'array':
                return convBuf.bind(data);
            case 'buffer':
                return convBuf.bind(data);
            case 'arraybuffer':
                return convBuf.bind(new Uint8Array(data));
            case 'view':
                return convBuf.bind(new Uint8Array(data.buffer, data.byteOffset, data.byteLength));
            case 'blob':
                return convBlob.bind(data);
            }
        };
        var slice = function (data, offset) {
            switch (util.getDataType(data)) {
            case 'string':
                return data.slice(offset);
            case 'array':
                return data.slice(offset);
            case 'buffer':
                return data.slice(offset);
            case 'arraybuffer':
                return data.slice(offset);
            case 'view':
                return data.buffer.slice(offset);
            }
        };
        var // Precompute 00 - ff strings
        precomputedHex = new Array(256);
        for (var i = 0; i < 256; i++) {
            precomputedHex[i] = (i < 16 ? '0' : '') + i.toString(16);
        }
        var // Convert an ArrayBuffer into its hexadecimal string representation.
        hex = function (arrayBuffer) {
            var binarray = new Uint8Array(arrayBuffer);
            var res = new Array(arrayBuffer.byteLength);
            for (var i$2 = 0; i$2 < res.length; i$2++) {
                res[i$2] = precomputedHex[binarray[i$2]];
            }
            return res.join('');
        };
        var ceilHeapSize = function (v) {
            // The asm.js spec says:
            // The heap object's byteLength must be either
            // 2^n for n in [12, 24) or 2^24 * n for n ≥ 1.
            // Also, byteLengths smaller than 2^16 are deprecated.
            var p;
            if (// If v is smaller than 2^16, the smallest possible solution
                // is 2^16.
                v <= 65536)
                return 65536;
            if (// If v < 2^24, we round up to 2^n,
                // otherwise we round up to 2^24 * n.
                v < 16777216) {
                for (p = 1; p < v; p = p << 1);
            } else {
                for (p = 16777216; p < v; p += 16777216);
            }
            return p;
        };
        var // Initialize the internal data structures to a new capacity.
        init = function (size) {
            if (size % 64 > 0) {
                throw new Error('Chunk size must be a multiple of 128 bit');
            }
            self$2.offset = 0;
            self$2.maxChunkLen = size;
            self$2.padMaxChunkLen = padlen(size);
            // The size of the heap is the sum of:
            // 1. The padded input message size
            // 2. The extended space the algorithm needs (320 byte)
            // 3. The 160 bit state the algoritm uses
            self$2.heap = new ArrayBuffer(ceilHeapSize(self$2.padMaxChunkLen + 320 + 20));
            self$2.h32 = new Int32Array(self$2.heap);
            self$2.h8 = new Int8Array(self$2.heap);
            self$2.core = new Rusha._core({
                Int32Array: Int32Array,
                DataView: DataView
            }, {}, self$2.heap);
            self$2.buffer = null;
        };
        // Iinitializethe datastructures according
        // to a chunk siyze.
        init(chunkSize || 64 * 1024);
        var initState = function (heap, padMsgLen) {
            self$2.offset = 0;
            var io = new Int32Array(heap, padMsgLen + 320, 5);
            io[0] = 1732584193;
            io[1] = -271733879;
            io[2] = -1732584194;
            io[3] = 271733878;
            io[4] = -1009589776;
        };
        var padChunk = function (chunkLen, msgLen) {
            var padChunkLen = padlen(chunkLen);
            var view = new Int32Array(self$2.heap, 0, padChunkLen >> 2);
            padZeroes(view, chunkLen);
            padData(view, chunkLen, msgLen);
            return padChunkLen;
        };
        var // Write data to the heap.
        write = function (data, chunkOffset, chunkLen, off) {
            convFn(data)(self$2.h8, self$2.h32, chunkOffset, chunkLen, off || 0);
        };
        var // Initialize and call the RushaCore,
        // assuming an input buffer of length len * 4.
        coreCall = function (data, chunkOffset, chunkLen, msgLen, finalize) {
            var padChunkLen = chunkLen;
            write(data, chunkOffset, chunkLen);
            if (finalize) {
                padChunkLen = padChunk(chunkLen, msgLen);
            }
            self$2.core.hash(padChunkLen, self$2.padMaxChunkLen);
        };
        var getRawDigest = function (heap, padMaxChunkLen) {
            var io = new Int32Array(heap, padMaxChunkLen + 320, 5);
            var out = new Int32Array(5);
            var arr = new DataView(out.buffer);
            arr.setInt32(0, io[0], false);
            arr.setInt32(4, io[1], false);
            arr.setInt32(8, io[2], false);
            arr.setInt32(12, io[3], false);
            arr.setInt32(16, io[4], false);
            return out;
        };
        var // Calculate the hash digest as an array of 5 32bit integers.
        rawDigest = this.rawDigest = function (str) {
            var msgLen = str.byteLength || str.length || str.size || 0;
            initState(self$2.heap, self$2.padMaxChunkLen);
            var chunkOffset = 0, chunkLen = self$2.maxChunkLen;
            for (chunkOffset = 0; msgLen > chunkOffset + chunkLen; chunkOffset += chunkLen) {
                coreCall(str, chunkOffset, chunkLen, msgLen, false);
            }
            coreCall(str, chunkOffset, msgLen - chunkOffset, msgLen, true);
            return getRawDigest(self$2.heap, self$2.padMaxChunkLen);
        };
        // The digest and digestFrom* interface returns the hash digest
        // as a hex string.
        this.digest = this.digestFromString = this.digestFromBuffer = this.digestFromArrayBuffer = function (str) {
            return hex(rawDigest(str).buffer);
        };
        this.resetState = function () {
            initState(self$2.heap, self$2.padMaxChunkLen);
            return this;
        };
        this.append = function (chunk) {
            var chunkOffset = 0;
            var chunkLen = chunk.byteLength || chunk.length || chunk.size || 0;
            var turnOffset = self$2.offset % self$2.maxChunkLen;
            var inputLen;
            self$2.offset += chunkLen;
            while (chunkOffset < chunkLen) {
                inputLen = Math.min(chunkLen - chunkOffset, self$2.maxChunkLen - turnOffset);
                write(chunk, chunkOffset, inputLen, turnOffset);
                turnOffset += inputLen;
                chunkOffset += inputLen;
                if (turnOffset === self$2.maxChunkLen) {
                    self$2.core.hash(self$2.maxChunkLen, self$2.padMaxChunkLen);
                    turnOffset = 0;
                }
            }
            return this;
        };
        this.getState = function () {
            var turnOffset = self$2.offset % self$2.maxChunkLen;
            var heap;
            if (!turnOffset) {
                var io = new Int32Array(self$2.heap, self$2.padMaxChunkLen + 320, 5);
                heap = io.buffer.slice(io.byteOffset, io.byteOffset + io.byteLength);
            } else {
                heap = self$2.heap.slice(0);
            }
            return {
                offset: self$2.offset,
                heap: heap
            };
        };
        this.setState = function (state) {
            self$2.offset = state.offset;
            if (state.heap.byteLength === 20) {
                var io = new Int32Array(self$2.heap, self$2.padMaxChunkLen + 320, 5);
                io.set(new Int32Array(state.heap));
            } else {
                self$2.h32.set(new Int32Array(state.heap));
            }
            return this;
        };
        var rawEnd = this.rawEnd = function () {
            var msgLen = self$2.offset;
            var chunkLen = msgLen % self$2.maxChunkLen;
            var padChunkLen = padChunk(chunkLen, msgLen);
            self$2.core.hash(padChunkLen, self$2.padMaxChunkLen);
            var result = getRawDigest(self$2.heap, self$2.padMaxChunkLen);
            initState(self$2.heap, self$2.padMaxChunkLen);
            return result;
        };
        this.end = function () {
            return hex(rawEnd().buffer);
        };
    }
    ;
    // The low-level RushCore module provides the heart of Rusha,
    // a high-speed sha1 implementation working on an Int32Array heap.
    // At first glance, the implementation seems complicated, however
    // with the SHA1 spec at hand, it is obvious this almost a textbook
    // implementation that has a few functions hand-inlined and a few loops
    // hand-unrolled.
    Rusha._core = function RushaCore(stdlib, foreign, heap) {
        'use asm';
        var H = new stdlib.Int32Array(heap);
        function hash(k, x) {
            // k in bytes
            k = k | 0;
            x = x | 0;
            var i = 0, j = 0, y0 = 0, z0 = 0, y1 = 0, z1 = 0, y2 = 0, z2 = 0, y3 = 0, z3 = 0, y4 = 0, z4 = 0, t0 = 0, t1 = 0;
            y0 = H[x + 320 >> 2] | 0;
            y1 = H[x + 324 >> 2] | 0;
            y2 = H[x + 328 >> 2] | 0;
            y3 = H[x + 332 >> 2] | 0;
            y4 = H[x + 336 >> 2] | 0;
            for (i = 0; (i | 0) < (k | 0); i = i + 64 | 0) {
                z0 = y0;
                z1 = y1;
                z2 = y2;
                z3 = y3;
                z4 = y4;
                for (j = 0; (j | 0) < 64; j = j + 4 | 0) {
                    t1 = H[i + j >> 2] | 0;
                    t0 = ((y0 << 5 | y0 >>> 27) + (y1 & y2 | ~y1 & y3) | 0) + ((t1 + y4 | 0) + 1518500249 | 0) | 0;
                    y4 = y3;
                    y3 = y2;
                    y2 = y1 << 30 | y1 >>> 2;
                    y1 = y0;
                    y0 = t0;
                    H[k + j >> 2] = t1;
                }
                for (j = k + 64 | 0; (j | 0) < (k + 80 | 0); j = j + 4 | 0) {
                    t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31;
                    t0 = ((y0 << 5 | y0 >>> 27) + (y1 & y2 | ~y1 & y3) | 0) + ((t1 + y4 | 0) + 1518500249 | 0) | 0;
                    y4 = y3;
                    y3 = y2;
                    y2 = y1 << 30 | y1 >>> 2;
                    y1 = y0;
                    y0 = t0;
                    H[j >> 2] = t1;
                }
                for (j = k + 80 | 0; (j | 0) < (k + 160 | 0); j = j + 4 | 0) {
                    t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31;
                    t0 = ((y0 << 5 | y0 >>> 27) + (y1 ^ y2 ^ y3) | 0) + ((t1 + y4 | 0) + 1859775393 | 0) | 0;
                    y4 = y3;
                    y3 = y2;
                    y2 = y1 << 30 | y1 >>> 2;
                    y1 = y0;
                    y0 = t0;
                    H[j >> 2] = t1;
                }
                for (j = k + 160 | 0; (j | 0) < (k + 240 | 0); j = j + 4 | 0) {
                    t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31;
                    t0 = ((y0 << 5 | y0 >>> 27) + (y1 & y2 | y1 & y3 | y2 & y3) | 0) + ((t1 + y4 | 0) - 1894007588 | 0) | 0;
                    y4 = y3;
                    y3 = y2;
                    y2 = y1 << 30 | y1 >>> 2;
                    y1 = y0;
                    y0 = t0;
                    H[j >> 2] = t1;
                }
                for (j = k + 240 | 0; (j | 0) < (k + 320 | 0); j = j + 4 | 0) {
                    t1 = (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) << 1 | (H[j - 12 >> 2] ^ H[j - 32 >> 2] ^ H[j - 56 >> 2] ^ H[j - 64 >> 2]) >>> 31;
                    t0 = ((y0 << 5 | y0 >>> 27) + (y1 ^ y2 ^ y3) | 0) + ((t1 + y4 | 0) - 899497514 | 0) | 0;
                    y4 = y3;
                    y3 = y2;
                    y2 = y1 << 30 | y1 >>> 2;
                    y1 = y0;
                    y0 = t0;
                    H[j >> 2] = t1;
                }
                y0 = y0 + z0 | 0;
                y1 = y1 + z1 | 0;
                y2 = y2 + z2 | 0;
                y3 = y3 + z3 | 0;
                y4 = y4 + z4 | 0;
            }
            H[x + 320 >> 2] = y0;
            H[x + 324 >> 2] = y1;
            H[x + 328 >> 2] = y2;
            H[x + 332 >> 2] = y3;
            H[x + 336 >> 2] = y4;
        }
        return { hash: hash };
    };

    if (// If we're running in a DOM context, export
        // the Rusha object to toplevel.
        typeof window !== 'undefined') {
        window.Rusha = Rusha;
    } else 
    if (// If we'e running in Node.JS, export a module.
        typeof module !== 'undefined') {
        module.exports = Rusha;
    } 

    if (// If we're running in a webworker, accept
        // messages containing a jobid and a buffer
        // or blob object, and return the hash result.
        typeof FileReaderSync !== 'undefined') {
        var reader = new FileReaderSync();
        var hashData = function hash(hasher, data, cb) {
            try {
                return cb(null, hasher.digest(data));
            } catch (e) {
                return cb(e);
            }
        };
        var hashFile = function hashArrayBuffer(hasher, readTotal, blockSize, file, cb) {
            var reader$2 = new self.FileReader();
            reader$2.onloadend = function onloadend() {
                var buffer = reader$2.result;
                readTotal += reader$2.result.byteLength;
                try {
                    hasher.append(buffer);
                } catch (e) {
                    cb(e);
                    return;
                }
                if (readTotal < file.size) {
                    hashFile(hasher, readTotal, blockSize, file, cb);
                } else {
                    cb(null, hasher.end());
                }
            };
            reader$2.readAsArrayBuffer(file.slice(readTotal, readTotal + blockSize));
        };
        self.onmessage = function onMessage(event) {
            var data = event.data.data, file = event.data.file, id = event.data.id;
            if (typeof id === 'undefined')
                return;
            if (!file && !data)
                return;
            var blockSize = event.data.blockSize || 4 * 1024 * 1024;
            var hasher = new Rusha(blockSize);
            hasher.resetState();
            var done = function done$2(err, hash) {
                if (!err) {
                    self.postMessage({
                        id: id,
                        hash: hash
                    });
                } else {
                    self.postMessage({
                        id: id,
                        error: err.name
                    });
                }
            };
            if (data)
                hashData(hasher, data, done);
            if (file)
                hashFile(hasher, 0, blockSize, file, done);
        };
    }
}());
(function(f){if(typeof exports==="object"&&typeof module!=="undefined"){module.exports=f()}else if(typeof define==="function"&&define.amd){define([],f)}else{var g;if(typeof window!=="undefined"){g=window}else if(typeof global!=="undefined"){g=global}else if(typeof self!=="undefined"){g=self}else{g=this}g.adapter = f()}})(function(){var define,module,exports;return (function(){function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,function(r){var n=e[i][1][r];return o(n||r)},p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i<t.length;i++)o(t[i]);return o}return r})()({1:[function(require,module,exports){
/*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */

'use strict';

var _adapter_factory = require('./adapter_factory.js');

var adapter = (0, _adapter_factory.adapterFactory)({ window: window });
module.exports = adapter; // this is the difference from adapter_core.

},{"./adapter_factory.js":2}],2:[function(require,module,exports){
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.adapterFactory = adapterFactory;

var _utils = require('./utils');

var utils = _interopRequireWildcard(_utils);

var _chrome_shim = require('./chrome/chrome_shim');

var chromeShim = _interopRequireWildcard(_chrome_shim);

var _edge_shim = require('./edge/edge_shim');

var edgeShim = _interopRequireWildcard(_edge_shim);

var _firefox_shim = require('./firefox/firefox_shim');

var firefoxShim = _interopRequireWildcard(_firefox_shim);

var _safari_shim = require('./safari/safari_shim');

var safariShim = _interopRequireWildcard(_safari_shim);

var _common_shim = require('./common_shim');

var commonShim = _interopRequireWildcard(_common_shim);

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

// Shimming starts here.
/*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
function adapterFactory() {
  var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
      window = _ref.window;

  var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {
    shimChrome: true,
    shimFirefox: true,
    shimEdge: true,
    shimSafari: true
  };

  // Utils.
  var logging = utils.log;
  var browserDetails = utils.detectBrowser(window);

  var adapter = {
    browserDetails: browserDetails,
    commonShim: commonShim,
    extractVersion: utils.extractVersion,
    disableLog: utils.disableLog,
    disableWarnings: utils.disableWarnings
  };

  // Shim browser if found.
  switch (browserDetails.browser) {
    case 'chrome':
      if (!chromeShim || !chromeShim.shimPeerConnection || !options.shimChrome) {
        logging('Chrome shim is not included in this adapter release.');
        return adapter;
      }
      logging('adapter.js shimming chrome.');
      // Export to the adapter global object visible in the browser.
      adapter.browserShim = chromeShim;

      chromeShim.shimGetUserMedia(window);
      chromeShim.shimMediaStream(window);
      chromeShim.shimPeerConnection(window);
      chromeShim.shimOnTrack(window);
      chromeShim.shimAddTrackRemoveTrack(window);
      chromeShim.shimGetSendersWithDtmf(window);
      chromeShim.shimGetStats(window);
      chromeShim.shimSenderReceiverGetStats(window);
      chromeShim.fixNegotiationNeeded(window);

      commonShim.shimRTCIceCandidate(window);
      commonShim.shimConnectionState(window);
      commonShim.shimMaxMessageSize(window);
      commonShim.shimSendThrowTypeError(window);
      commonShim.removeAllowExtmapMixed(window);
      break;
    case 'firefox':
      if (!firefoxShim || !firefoxShim.shimPeerConnection || !options.shimFirefox) {
        logging('Firefox shim is not included in this adapter release.');
        return adapter;
      }
      logging('adapter.js shimming firefox.');
      // Export to the adapter global object visible in the browser.
      adapter.browserShim = firefoxShim;

      firefoxShim.shimGetUserMedia(window);
      firefoxShim.shimPeerConnection(window);
      firefoxShim.shimOnTrack(window);
      firefoxShim.shimRemoveStream(window);
      firefoxShim.shimSenderGetStats(window);
      firefoxShim.shimReceiverGetStats(window);
      firefoxShim.shimRTCDataChannel(window);
      firefoxShim.shimAddTransceiver(window);
      firefoxShim.shimCreateOffer(window);
      firefoxShim.shimCreateAnswer(window);

      commonShim.shimRTCIceCandidate(window);
      commonShim.shimConnectionState(window);
      commonShim.shimMaxMessageSize(window);
      commonShim.shimSendThrowTypeError(window);
      break;
    case 'edge':
      if (!edgeShim || !edgeShim.shimPeerConnection || !options.shimEdge) {
        logging('MS edge shim is not included in this adapter release.');
        return adapter;
      }
      logging('adapter.js shimming edge.');
      // Export to the adapter global object visible in the browser.
      adapter.browserShim = edgeShim;

      edgeShim.shimGetUserMedia(window);
      edgeShim.shimGetDisplayMedia(window);
      edgeShim.shimPeerConnection(window);
      edgeShim.shimReplaceTrack(window);

      // the edge shim implements the full RTCIceCandidate object.

      commonShim.shimMaxMessageSize(window);
      commonShim.shimSendThrowTypeError(window);
      break;
    case 'safari':
      if (!safariShim || !options.shimSafari) {
        logging('Safari shim is not included in this adapter release.');
        return adapter;
      }
      logging('adapter.js shimming safari.');
      // Export to the adapter global object visible in the browser.
      adapter.browserShim = safariShim;

      safariShim.shimRTCIceServerUrls(window);
      safariShim.shimCreateOfferLegacy(window);
      safariShim.shimCallbacksAPI(window);
      safariShim.shimLocalStreamsAPI(window);
      safariShim.shimRemoteStreamsAPI(window);
      safariShim.shimTrackEventTransceiver(window);
      safariShim.shimGetUserMedia(window);

      commonShim.shimRTCIceCandidate(window);
      commonShim.shimMaxMessageSize(window);
      commonShim.shimSendThrowTypeError(window);
      commonShim.removeAllowExtmapMixed(window);
      break;
    default:
      logging('Unsupported browser!');
      break;
  }

  return adapter;
}

// Browser shims.

},{"./chrome/chrome_shim":3,"./common_shim":6,"./edge/edge_shim":7,"./firefox/firefox_shim":11,"./safari/safari_shim":14,"./utils":15}],3:[function(require,module,exports){

/*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.shimGetDisplayMedia = exports.shimGetUserMedia = undefined;

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

var _getusermedia = require('./getusermedia');

Object.defineProperty(exports, 'shimGetUserMedia', {
  enumerable: true,
  get: function get() {
    return _getusermedia.shimGetUserMedia;
  }
});

var _getdisplaymedia = require('./getdisplaymedia');

Object.defineProperty(exports, 'shimGetDisplayMedia', {
  enumerable: true,
  get: function get() {
    return _getdisplaymedia.shimGetDisplayMedia;
  }
});
exports.shimMediaStream = shimMediaStream;
exports.shimOnTrack = shimOnTrack;
exports.shimGetSendersWithDtmf = shimGetSendersWithDtmf;
exports.shimGetStats = shimGetStats;
exports.shimSenderReceiverGetStats = shimSenderReceiverGetStats;
exports.shimAddTrackRemoveTrackWithNative = shimAddTrackRemoveTrackWithNative;
exports.shimAddTrackRemoveTrack = shimAddTrackRemoveTrack;
exports.shimPeerConnection = shimPeerConnection;
exports.fixNegotiationNeeded = fixNegotiationNeeded;

var _utils = require('../utils.js');

var utils = _interopRequireWildcard(_utils);

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

function shimMediaStream(window) {
  window.MediaStream = window.MediaStream || window.webkitMediaStream;
}

function shimOnTrack(window) {
  if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && !('ontrack' in window.RTCPeerConnection.prototype)) {
    Object.defineProperty(window.RTCPeerConnection.prototype, 'ontrack', {
      get: function get() {
        return this._ontrack;
      },
      set: function set(f) {
        if (this._ontrack) {
          this.removeEventListener('track', this._ontrack);
        }
        this.addEventListener('track', this._ontrack = f);
      },

      enumerable: true,
      configurable: true
    });
    var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription;
    window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() {
      var _this = this;

      if (!this._ontrackpoly) {
        this._ontrackpoly = function (e) {
          // onaddstream does not fire when a track is added to an existing
          // stream. But stream.onaddtrack is implemented so we use that.
          e.stream.addEventListener('addtrack', function (te) {
            var receiver = void 0;
            if (window.RTCPeerConnection.prototype.getReceivers) {
              receiver = _this.getReceivers().find(function (r) {
                return r.track && r.track.id === te.track.id;
              });
            } else {
              receiver = { track: te.track };
            }

            var event = new Event('track');
            event.track = te.track;
            event.receiver = receiver;
            event.transceiver = { receiver: receiver };
            event.streams = [e.stream];
            _this.dispatchEvent(event);
          });
          e.stream.getTracks().forEach(function (track) {
            var receiver = void 0;
            if (window.RTCPeerConnection.prototype.getReceivers) {
              receiver = _this.getReceivers().find(function (r) {
                return r.track && r.track.id === track.id;
              });
            } else {
              receiver = { track: track };
            }
            var event = new Event('track');
            event.track = track;
            event.receiver = receiver;
            event.transceiver = { receiver: receiver };
            event.streams = [e.stream];
            _this.dispatchEvent(event);
          });
        };
        this.addEventListener('addstream', this._ontrackpoly);
      }
      return origSetRemoteDescription.apply(this, arguments);
    };
  } else {
    // even if RTCRtpTransceiver is in window, it is only used and
    // emitted in unified-plan. Unfortunately this means we need
    // to unconditionally wrap the event.
    utils.wrapPeerConnectionEvent(window, 'track', function (e) {
      if (!e.transceiver) {
        Object.defineProperty(e, 'transceiver', { value: { receiver: e.receiver } });
      }
      return e;
    });
  }
}

function shimGetSendersWithDtmf(window) {
  // Overrides addTrack/removeTrack, depends on shimAddTrackRemoveTrack.
  if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && !('getSenders' in window.RTCPeerConnection.prototype) && 'createDTMFSender' in window.RTCPeerConnection.prototype) {
    var shimSenderWithDtmf = function shimSenderWithDtmf(pc, track) {
      return {
        track: track,
        get dtmf() {
          if (this._dtmf === undefined) {
            if (track.kind === 'audio') {
              this._dtmf = pc.createDTMFSender(track);
            } else {
              this._dtmf = null;
            }
          }
          return this._dtmf;
        },
        _pc: pc
      };
    };

    // augment addTrack when getSenders is not available.
    if (!window.RTCPeerConnection.prototype.getSenders) {
      window.RTCPeerConnection.prototype.getSenders = function getSenders() {
        this._senders = this._senders || [];
        return this._senders.slice(); // return a copy of the internal state.
      };
      var origAddTrack = window.RTCPeerConnection.prototype.addTrack;
      window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) {
        var sender = origAddTrack.apply(this, arguments);
        if (!sender) {
          sender = shimSenderWithDtmf(this, track);
          this._senders.push(sender);
        }
        return sender;
      };

      var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack;
      window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) {
        origRemoveTrack.apply(this, arguments);
        var idx = this._senders.indexOf(sender);
        if (idx !== -1) {
          this._senders.splice(idx, 1);
        }
      };
    }
    var origAddStream = window.RTCPeerConnection.prototype.addStream;
    window.RTCPeerConnection.prototype.addStream = function addStream(stream) {
      var _this2 = this;

      this._senders = this._senders || [];
      origAddStream.apply(this, [stream]);
      stream.getTracks().forEach(function (track) {
        _this2._senders.push(shimSenderWithDtmf(_this2, track));
      });
    };

    var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
    window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) {
      var _this3 = this;

      this._senders = this._senders || [];
      origRemoveStream.apply(this, [stream]);

      stream.getTracks().forEach(function (track) {
        var sender = _this3._senders.find(function (s) {
          return s.track === track;
        });
        if (sender) {
          // remove sender
          _this3._senders.splice(_this3._senders.indexOf(sender), 1);
        }
      });
    };
  } else if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && 'getSenders' in window.RTCPeerConnection.prototype && 'createDTMFSender' in window.RTCPeerConnection.prototype && window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) {
    var origGetSenders = window.RTCPeerConnection.prototype.getSenders;
    window.RTCPeerConnection.prototype.getSenders = function getSenders() {
      var _this4 = this;

      var senders = origGetSenders.apply(this, []);
      senders.forEach(function (sender) {
        return sender._pc = _this4;
      });
      return senders;
    };

    Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
      get: function get() {
        if (this._dtmf === undefined) {
          if (this.track.kind === 'audio') {
            this._dtmf = this._pc.createDTMFSender(this.track);
          } else {
            this._dtmf = null;
          }
        }
        return this._dtmf;
      }
    });
  }
}

function shimGetStats(window) {
  if (!window.RTCPeerConnection) {
    return;
  }

  var origGetStats = window.RTCPeerConnection.prototype.getStats;
  window.RTCPeerConnection.prototype.getStats = function getStats() {
    var _this5 = this;

    var _arguments = Array.prototype.slice.call(arguments),
        selector = _arguments[0],
        onSucc = _arguments[1],
        onErr = _arguments[2];

    // If selector is a function then we are in the old style stats so just
    // pass back the original getStats format to avoid breaking old users.


    if (arguments.length > 0 && typeof selector === 'function') {
      return origGetStats.apply(this, arguments);
    }

    // When spec-style getStats is supported, return those when called with
    // either no arguments or the selector argument is null.
    if (origGetStats.length === 0 && (arguments.length === 0 || typeof selector !== 'function')) {
      return origGetStats.apply(this, []);
    }

    var fixChromeStats_ = function fixChromeStats_(response) {
      var standardReport = {};
      var reports = response.result();
      reports.forEach(function (report) {
        var standardStats = {
          id: report.id,
          timestamp: report.timestamp,
          type: {
            localcandidate: 'local-candidate',
            remotecandidate: 'remote-candidate'
          }[report.type] || report.type
        };
        report.names().forEach(function (name) {
          standardStats[name] = report.stat(name);
        });
        standardReport[standardStats.id] = standardStats;
      });

      return standardReport;
    };

    // shim getStats with maplike support
    var makeMapStats = function makeMapStats(stats) {
      return new Map(Object.keys(stats).map(function (key) {
        return [key, stats[key]];
      }));
    };

    if (arguments.length >= 2) {
      var successCallbackWrapper_ = function successCallbackWrapper_(response) {
        onSucc(makeMapStats(fixChromeStats_(response)));
      };

      return origGetStats.apply(this, [successCallbackWrapper_, selector]);
    }

    // promise-support
    return new Promise(function (resolve, reject) {
      origGetStats.apply(_this5, [function (response) {
        resolve(makeMapStats(fixChromeStats_(response)));
      }, reject]);
    }).then(onSucc, onErr);
  };
}

function shimSenderReceiverGetStats(window) {
  if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && window.RTCRtpSender && window.RTCRtpReceiver)) {
    return;
  }

  // shim sender stats.
  if (!('getStats' in window.RTCRtpSender.prototype)) {
    var origGetSenders = window.RTCPeerConnection.prototype.getSenders;
    if (origGetSenders) {
      window.RTCPeerConnection.prototype.getSenders = function getSenders() {
        var _this6 = this;

        var senders = origGetSenders.apply(this, []);
        senders.forEach(function (sender) {
          return sender._pc = _this6;
        });
        return senders;
      };
    }

    var origAddTrack = window.RTCPeerConnection.prototype.addTrack;
    if (origAddTrack) {
      window.RTCPeerConnection.prototype.addTrack = function addTrack() {
        var sender = origAddTrack.apply(this, arguments);
        sender._pc = this;
        return sender;
      };
    }
    window.RTCRtpSender.prototype.getStats = function getStats() {
      var sender = this;
      return this._pc.getStats().then(function (result) {
        return (
          /* Note: this will include stats of all senders that
           *   send a track with the same id as sender.track as
           *   it is not possible to identify the RTCRtpSender.
           */
          utils.filterStats(result, sender.track, true)
        );
      });
    };
  }

  // shim receiver stats.
  if (!('getStats' in window.RTCRtpReceiver.prototype)) {
    var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers;
    if (origGetReceivers) {
      window.RTCPeerConnection.prototype.getReceivers = function getReceivers() {
        var _this7 = this;

        var receivers = origGetReceivers.apply(this, []);
        receivers.forEach(function (receiver) {
          return receiver._pc = _this7;
        });
        return receivers;
      };
    }
    utils.wrapPeerConnectionEvent(window, 'track', function (e) {
      e.receiver._pc = e.srcElement;
      return e;
    });
    window.RTCRtpReceiver.prototype.getStats = function getStats() {
      var receiver = this;
      return this._pc.getStats().then(function (result) {
        return utils.filterStats(result, receiver.track, false);
      });
    };
  }

  if (!('getStats' in window.RTCRtpSender.prototype && 'getStats' in window.RTCRtpReceiver.prototype)) {
    return;
  }

  // shim RTCPeerConnection.getStats(track).
  var origGetStats = window.RTCPeerConnection.prototype.getStats;
  window.RTCPeerConnection.prototype.getStats = function getStats() {
    if (arguments.length > 0 && arguments[0] instanceof window.MediaStreamTrack) {
      var track = arguments[0];
      var sender = void 0;
      var receiver = void 0;
      var err = void 0;
      this.getSenders().forEach(function (s) {
        if (s.track === track) {
          if (sender) {
            err = true;
          } else {
            sender = s;
          }
        }
      });
      this.getReceivers().forEach(function (r) {
        if (r.track === track) {
          if (receiver) {
            err = true;
          } else {
            receiver = r;
          }
        }
        return r.track === track;
      });
      if (err || sender && receiver) {
        return Promise.reject(new DOMException('There are more than one sender or receiver for the track.', 'InvalidAccessError'));
      } else if (sender) {
        return sender.getStats();
      } else if (receiver) {
        return receiver.getStats();
      }
      return Promise.reject(new DOMException('There is no sender or receiver for the track.', 'InvalidAccessError'));
    }
    return origGetStats.apply(this, arguments);
  };
}

function shimAddTrackRemoveTrackWithNative(window) {
  // shim addTrack/removeTrack with native variants in order to make
  // the interactions with legacy getLocalStreams behave as in other browsers.
  // Keeps a mapping stream.id => [stream, rtpsenders...]
  window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() {
    var _this8 = this;

    this._shimmedLocalStreams = this._shimmedLocalStreams || {};
    return Object.keys(this._shimmedLocalStreams).map(function (streamId) {
      return _this8._shimmedLocalStreams[streamId][0];
    });
  };

  var origAddTrack = window.RTCPeerConnection.prototype.addTrack;
  window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) {
    if (!stream) {
      return origAddTrack.apply(this, arguments);
    }
    this._shimmedLocalStreams = this._shimmedLocalStreams || {};

    var sender = origAddTrack.apply(this, arguments);
    if (!this._shimmedLocalStreams[stream.id]) {
      this._shimmedLocalStreams[stream.id] = [stream, sender];
    } else if (this._shimmedLocalStreams[stream.id].indexOf(sender) === -1) {
      this._shimmedLocalStreams[stream.id].push(sender);
    }
    return sender;
  };

  var origAddStream = window.RTCPeerConnection.prototype.addStream;
  window.RTCPeerConnection.prototype.addStream = function addStream(stream) {
    var _this9 = this;

    this._shimmedLocalStreams = this._shimmedLocalStreams || {};

    stream.getTracks().forEach(function (track) {
      var alreadyExists = _this9.getSenders().find(function (s) {
        return s.track === track;
      });
      if (alreadyExists) {
        throw new DOMException('Track already exists.', 'InvalidAccessError');
      }
    });
    var existingSenders = this.getSenders();
    origAddStream.apply(this, arguments);
    var newSenders = this.getSenders().filter(function (newSender) {
      return existingSenders.indexOf(newSender) === -1;
    });
    this._shimmedLocalStreams[stream.id] = [stream].concat(newSenders);
  };

  var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
  window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) {
    this._shimmedLocalStreams = this._shimmedLocalStreams || {};
    delete this._shimmedLocalStreams[stream.id];
    return origRemoveStream.apply(this, arguments);
  };

  var origRemoveTrack = window.RTCPeerConnection.prototype.removeTrack;
  window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) {
    var _this10 = this;

    this._shimmedLocalStreams = this._shimmedLocalStreams || {};
    if (sender) {
      Object.keys(this._shimmedLocalStreams).forEach(function (streamId) {
        var idx = _this10._shimmedLocalStreams[streamId].indexOf(sender);
        if (idx !== -1) {
          _this10._shimmedLocalStreams[streamId].splice(idx, 1);
        }
        if (_this10._shimmedLocalStreams[streamId].length === 1) {
          delete _this10._shimmedLocalStreams[streamId];
        }
      });
    }
    return origRemoveTrack.apply(this, arguments);
  };
}

function shimAddTrackRemoveTrack(window) {
  if (!window.RTCPeerConnection) {
    return;
  }
  var browserDetails = utils.detectBrowser(window);
  // shim addTrack and removeTrack.
  if (window.RTCPeerConnection.prototype.addTrack && browserDetails.version >= 65) {
    return shimAddTrackRemoveTrackWithNative(window);
  }

  // also shim pc.getLocalStreams when addTrack is shimmed
  // to return the original streams.
  var origGetLocalStreams = window.RTCPeerConnection.prototype.getLocalStreams;
  window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() {
    var _this11 = this;

    var nativeStreams = origGetLocalStreams.apply(this);
    this._reverseStreams = this._reverseStreams || {};
    return nativeStreams.map(function (stream) {
      return _this11._reverseStreams[stream.id];
    });
  };

  var origAddStream = window.RTCPeerConnection.prototype.addStream;
  window.RTCPeerConnection.prototype.addStream = function addStream(stream) {
    var _this12 = this;

    this._streams = this._streams || {};
    this._reverseStreams = this._reverseStreams || {};

    stream.getTracks().forEach(function (track) {
      var alreadyExists = _this12.getSenders().find(function (s) {
        return s.track === track;
      });
      if (alreadyExists) {
        throw new DOMException('Track already exists.', 'InvalidAccessError');
      }
    });
    // Add identity mapping for consistency with addTrack.
    // Unless this is being used with a stream from addTrack.
    if (!this._reverseStreams[stream.id]) {
      var newStream = new window.MediaStream(stream.getTracks());
      this._streams[stream.id] = newStream;
      this._reverseStreams[newStream.id] = stream;
      stream = newStream;
    }
    origAddStream.apply(this, [stream]);
  };

  var origRemoveStream = window.RTCPeerConnection.prototype.removeStream;
  window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) {
    this._streams = this._streams || {};
    this._reverseStreams = this._reverseStreams || {};

    origRemoveStream.apply(this, [this._streams[stream.id] || stream]);
    delete this._reverseStreams[this._streams[stream.id] ? this._streams[stream.id].id : stream.id];
    delete this._streams[stream.id];
  };

  window.RTCPeerConnection.prototype.addTrack = function addTrack(track, stream) {
    var _this13 = this;

    if (this.signalingState === 'closed') {
      throw new DOMException('The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError');
    }
    var streams = [].slice.call(arguments, 1);
    if (streams.length !== 1 || !streams[0].getTracks().find(function (t) {
      return t === track;
    })) {
      // this is not fully correct but all we can manage without
      // [[associated MediaStreams]] internal slot.
      throw new DOMException('The adapter.js addTrack polyfill only supports a single ' + ' stream which is associated with the specified track.', 'NotSupportedError');
    }

    var alreadyExists = this.getSenders().find(function (s) {
      return s.track === track;
    });
    if (alreadyExists) {
      throw new DOMException('Track already exists.', 'InvalidAccessError');
    }

    this._streams = this._streams || {};
    this._reverseStreams = this._reverseStreams || {};
    var oldStream = this._streams[stream.id];
    if (oldStream) {
      // this is using odd Chrome behaviour, use with caution:
      // https://bugs.chromium.org/p/webrtc/issues/detail?id=7815
      // Note: we rely on the high-level addTrack/dtmf shim to
      // create the sender with a dtmf sender.
      oldStream.addTrack(track);

      // Trigger ONN async.
      Promise.resolve().then(function () {
        _this13.dispatchEvent(new Event('negotiationneeded'));
      });
    } else {
      var newStream = new window.MediaStream([track]);
      this._streams[stream.id] = newStream;
      this._reverseStreams[newStream.id] = stream;
      this.addStream(newStream);
    }
    return this.getSenders().find(function (s) {
      return s.track === track;
    });
  };

  // replace the internal stream id with the external one and
  // vice versa.
  function replaceInternalStreamId(pc, description) {
    var sdp = description.sdp;
    Object.keys(pc._reverseStreams || []).forEach(function (internalId) {
      var externalStream = pc._reverseStreams[internalId];
      var internalStream = pc._streams[externalStream.id];
      sdp = sdp.replace(new RegExp(internalStream.id, 'g'), externalStream.id);
    });
    return new RTCSessionDescription({
      type: description.type,
      sdp: sdp
    });
  }
  function replaceExternalStreamId(pc, description) {
    var sdp = description.sdp;
    Object.keys(pc._reverseStreams || []).forEach(function (internalId) {
      var externalStream = pc._reverseStreams[internalId];
      var internalStream = pc._streams[externalStream.id];
      sdp = sdp.replace(new RegExp(externalStream.id, 'g'), internalStream.id);
    });
    return new RTCSessionDescription({
      type: description.type,
      sdp: sdp
    });
  }
  ['createOffer', 'createAnswer'].forEach(function (method) {
    var nativeMethod = window.RTCPeerConnection.prototype[method];
    var methodObj = _defineProperty({}, method, function () {
      var _this14 = this;

      var args = arguments;
      var isLegacyCall = arguments.length && typeof arguments[0] === 'function';
      if (isLegacyCall) {
        return nativeMethod.apply(this, [function (description) {
          var desc = replaceInternalStreamId(_this14, description);
          args[0].apply(null, [desc]);
        }, function (err) {
          if (args[1]) {
            args[1].apply(null, err);
          }
        }, arguments[2]]);
      }
      return nativeMethod.apply(this, arguments).then(function (description) {
        return replaceInternalStreamId(_this14, description);
      });
    });
    window.RTCPeerConnection.prototype[method] = methodObj[method];
  });

  var origSetLocalDescription = window.RTCPeerConnection.prototype.setLocalDescription;
  window.RTCPeerConnection.prototype.setLocalDescription = function setLocalDescription() {
    if (!arguments.length || !arguments[0].type) {
      return origSetLocalDescription.apply(this, arguments);
    }
    arguments[0] = replaceExternalStreamId(this, arguments[0]);
    return origSetLocalDescription.apply(this, arguments);
  };

  // TODO: mangle getStats: https://w3c.github.io/webrtc-stats/#dom-rtcmediastreamstats-streamidentifier

  var origLocalDescription = Object.getOwnPropertyDescriptor(window.RTCPeerConnection.prototype, 'localDescription');
  Object.defineProperty(window.RTCPeerConnection.prototype, 'localDescription', {
    get: function get() {
      var description = origLocalDescription.get.apply(this);
      if (description.type === '') {
        return description;
      }
      return replaceInternalStreamId(this, description);
    }
  });

  window.RTCPeerConnection.prototype.removeTrack = function removeTrack(sender) {
    var _this15 = this;

    if (this.signalingState === 'closed') {
      throw new DOMException('The RTCPeerConnection\'s signalingState is \'closed\'.', 'InvalidStateError');
    }
    // We can not yet check for sender instanceof RTCRtpSender
    // since we shim RTPSender. So we check if sender._pc is set.
    if (!sender._pc) {
      throw new DOMException('Argument 1 of RTCPeerConnection.removeTrack ' + 'does not implement interface RTCRtpSender.', 'TypeError');
    }
    var isLocal = sender._pc === this;
    if (!isLocal) {
      throw new DOMException('Sender was not created by this connection.', 'InvalidAccessError');
    }

    // Search for the native stream the senders track belongs to.
    this._streams = this._streams || {};
    var stream = void 0;
    Object.keys(this._streams).forEach(function (streamid) {
      var hasTrack = _this15._streams[streamid].getTracks().find(function (track) {
        return sender.track === track;
      });
      if (hasTrack) {
        stream = _this15._streams[streamid];
      }
    });

    if (stream) {
      if (stream.getTracks().length === 1) {
        // if this is the last track of the stream, remove the stream. This
        // takes care of any shimmed _senders.
        this.removeStream(this._reverseStreams[stream.id]);
      } else {
        // relying on the same odd chrome behaviour as above.
        stream.removeTrack(sender.track);
      }
      this.dispatchEvent(new Event('negotiationneeded'));
    }
  };
}

function shimPeerConnection(window) {
  var browserDetails = utils.detectBrowser(window);

  if (!window.RTCPeerConnection && window.webkitRTCPeerConnection) {
    // very basic support for old versions.
    window.RTCPeerConnection = window.webkitRTCPeerConnection;
  }
  if (!window.RTCPeerConnection) {
    return;
  }

  // shim implicit creation of RTCSessionDescription/RTCIceCandidate
  if (browserDetails.version < 53) {
    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'].forEach(function (method) {
      var nativeMethod = window.RTCPeerConnection.prototype[method];
      var methodObj = _defineProperty({}, method, function () {
        arguments[0] = new (method === 'addIceCandidate' ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]);
        return nativeMethod.apply(this, arguments);
      });
      window.RTCPeerConnection.prototype[method] = methodObj[method];
    });
  }

  // support for addIceCandidate(null or undefined)
  var nativeAddIceCandidate = window.RTCPeerConnection.prototype.addIceCandidate;
  window.RTCPeerConnection.prototype.addIceCandidate = function addIceCandidate() {
    if (!arguments[0]) {
      if (arguments[1]) {
        arguments[1].apply(null);
      }
      return Promise.resolve();
    }
    // Firefox 68+ emits and processes {candidate: "", ...}, ignore
    // in older versions. Native support planned for Chrome M77.
    if (browserDetails.version < 78 && arguments[0] && arguments[0].candidate === '') {
      return Promise.resolve();
    }
    return nativeAddIceCandidate.apply(this, arguments);
  };
}

function fixNegotiationNeeded(window) {
  utils.wrapPeerConnectionEvent(window, 'negotiationneeded', function (e) {
    var pc = e.target;
    if (pc.signalingState !== 'stable') {
      return;
    }
    return e;
  });
}

},{"../utils.js":15,"./getdisplaymedia":4,"./getusermedia":5}],4:[function(require,module,exports){
/*
 *  Copyright (c) 2018 The adapter.js project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.shimGetDisplayMedia = shimGetDisplayMedia;
function shimGetDisplayMedia(window, getSourceId) {
  if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) {
    return;
  }
  if (!window.navigator.mediaDevices) {
    return;
  }
  // getSourceId is a function that returns a promise resolving with
  // the sourceId of the screen/window/tab to be shared.
  if (typeof getSourceId !== 'function') {
    console.error('shimGetDisplayMedia: getSourceId argument is not ' + 'a function');
    return;
  }
  window.navigator.mediaDevices.getDisplayMedia = function getDisplayMedia(constraints) {
    return getSourceId(constraints).then(function (sourceId) {
      var widthSpecified = constraints.video && constraints.video.width;
      var heightSpecified = constraints.video && constraints.video.height;
      var frameRateSpecified = constraints.video && constraints.video.frameRate;
      constraints.video = {
        mandatory: {
          chromeMediaSource: 'desktop',
          chromeMediaSourceId: sourceId,
          maxFrameRate: frameRateSpecified || 3
        }
      };
      if (widthSpecified) {
        constraints.video.mandatory.maxWidth = widthSpecified;
      }
      if (heightSpecified) {
        constraints.video.mandatory.maxHeight = heightSpecified;
      }
      return window.navigator.mediaDevices.getUserMedia(constraints);
    });
  };
}

},{}],5:[function(require,module,exports){
/*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

exports.shimGetUserMedia = shimGetUserMedia;

var _utils = require('../utils.js');

var utils = _interopRequireWildcard(_utils);

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

var logging = utils.log;

function shimGetUserMedia(window) {
  var navigator = window && window.navigator;

  if (!navigator.mediaDevices) {
    return;
  }

  var browserDetails = utils.detectBrowser(window);

  var constraintsToChrome_ = function constraintsToChrome_(c) {
    if ((typeof c === 'undefined' ? 'undefined' : _typeof(c)) !== 'object' || c.mandatory || c.optional) {
      return c;
    }
    var cc = {};
    Object.keys(c).forEach(function (key) {
      if (key === 'require' || key === 'advanced' || key === 'mediaSource') {
        return;
      }
      var r = _typeof(c[key]) === 'object' ? c[key] : { ideal: c[key] };
      if (r.exact !== undefined && typeof r.exact === 'number') {
        r.min = r.max = r.exact;
      }
      var oldname_ = function oldname_(prefix, name) {
        if (prefix) {
          return prefix + name.charAt(0).toUpperCase() + name.slice(1);
        }
        return name === 'deviceId' ? 'sourceId' : name;
      };
      if (r.ideal !== undefined) {
        cc.optional = cc.optional || [];
        var oc = {};
        if (typeof r.ideal === 'number') {
          oc[oldname_('min', key)] = r.ideal;
          cc.optional.push(oc);
          oc = {};
          oc[oldname_('max', key)] = r.ideal;
          cc.optional.push(oc);
        } else {
          oc[oldname_('', key)] = r.ideal;
          cc.optional.push(oc);
        }
      }
      if (r.exact !== undefined && typeof r.exact !== 'number') {
        cc.mandatory = cc.mandatory || {};
        cc.mandatory[oldname_('', key)] = r.exact;
      } else {
        ['min', 'max'].forEach(function (mix) {
          if (r[mix] !== undefined) {
            cc.mandatory = cc.mandatory || {};
            cc.mandatory[oldname_(mix, key)] = r[mix];
          }
        });
      }
    });
    if (c.advanced) {
      cc.optional = (cc.optional || []).concat(c.advanced);
    }
    return cc;
  };

  var shimConstraints_ = function shimConstraints_(constraints, func) {
    if (browserDetails.version >= 61) {
      return func(constraints);
    }
    constraints = JSON.parse(JSON.stringify(constraints));
    if (constraints && _typeof(constraints.audio) === 'object') {
      var remap = function remap(obj, a, b) {
        if (a in obj && !(b in obj)) {
          obj[b] = obj[a];
          delete obj[a];
        }
      };
      constraints = JSON.parse(JSON.stringify(constraints));
      remap(constraints.audio, 'autoGainControl', 'googAutoGainControl');
      remap(constraints.audio, 'noiseSuppression', 'googNoiseSuppression');
      constraints.audio = constraintsToChrome_(constraints.audio);
    }
    if (constraints && _typeof(constraints.video) === 'object') {
      // Shim facingMode for mobile & surface pro.
      var face = constraints.video.facingMode;
      face = face && ((typeof face === 'undefined' ? 'undefined' : _typeof(face)) === 'object' ? face : { ideal: face });
      var getSupportedFacingModeLies = browserDetails.version < 66;

      if (face && (face.exact === 'user' || face.exact === 'environment' || face.ideal === 'user' || face.ideal === 'environment') && !(navigator.mediaDevices.getSupportedConstraints && navigator.mediaDevices.getSupportedConstraints().facingMode && !getSupportedFacingModeLies)) {
        delete constraints.video.facingMode;
        var matches = void 0;
        if (face.exact === 'environment' || face.ideal === 'environment') {
          matches = ['back', 'rear'];
        } else if (face.exact === 'user' || face.ideal === 'user') {
          matches = ['front'];
        }
        if (matches) {
          // Look for matches in label, or use last cam for back (typical).
          return navigator.mediaDevices.enumerateDevices().then(function (devices) {
            devices = devices.filter(function (d) {
              return d.kind === 'videoinput';
            });
            var dev = devices.find(function (d) {
              return matches.some(function (match) {
                return d.label.toLowerCase().includes(match);
              });
            });
            if (!dev && devices.length && matches.includes('back')) {
              dev = devices[devices.length - 1]; // more likely the back cam
            }
            if (dev) {
              constraints.video.deviceId = face.exact ? { exact: dev.deviceId } : { ideal: dev.deviceId };
            }
            constraints.video = constraintsToChrome_(constraints.video);
            logging('chrome: ' + JSON.stringify(constraints));
            return func(constraints);
          });
        }
      }
      constraints.video = constraintsToChrome_(constraints.video);
    }
    logging('chrome: ' + JSON.stringify(constraints));
    return func(constraints);
  };

  var shimError_ = function shimError_(e) {
    if (browserDetails.version >= 64) {
      return e;
    }
    return {
      name: {
        PermissionDeniedError: 'NotAllowedError',
        PermissionDismissedError: 'NotAllowedError',
        InvalidStateError: 'NotAllowedError',
        DevicesNotFoundError: 'NotFoundError',
        ConstraintNotSatisfiedError: 'OverconstrainedError',
        TrackStartError: 'NotReadableError',
        MediaDeviceFailedDueToShutdown: 'NotAllowedError',
        MediaDeviceKillSwitchOn: 'NotAllowedError',
        TabCaptureError: 'AbortError',
        ScreenCaptureError: 'AbortError',
        DeviceCaptureError: 'AbortError'
      }[e.name] || e.name,
      message: e.message,
      constraint: e.constraint || e.constraintName,
      toString: function toString() {
        return this.name + (this.message && ': ') + this.message;
      }
    };
  };

  var getUserMedia_ = function getUserMedia_(constraints, onSuccess, onError) {
    shimConstraints_(constraints, function (c) {
      navigator.webkitGetUserMedia(c, onSuccess, function (e) {
        if (onError) {
          onError(shimError_(e));
        }
      });
    });
  };
  navigator.getUserMedia = getUserMedia_.bind(navigator);

  // Even though Chrome 45 has navigator.mediaDevices and a getUserMedia
  // function which returns a Promise, it does not accept spec-style
  // constraints.
  if (navigator.mediaDevices.getUserMedia) {
    var origGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
    navigator.mediaDevices.getUserMedia = function (cs) {
      return shimConstraints_(cs, function (c) {
        return origGetUserMedia(c).then(function (stream) {
          if (c.audio && !stream.getAudioTracks().length || c.video && !stream.getVideoTracks().length) {
            stream.getTracks().forEach(function (track) {
              track.stop();
            });
            throw new DOMException('', 'NotFoundError');
          }
          return stream;
        }, function (e) {
          return Promise.reject(shimError_(e));
        });
      });
    };
  }
}

},{"../utils.js":15}],6:[function(require,module,exports){
/*
 *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

exports.shimRTCIceCandidate = shimRTCIceCandidate;
exports.shimMaxMessageSize = shimMaxMessageSize;
exports.shimSendThrowTypeError = shimSendThrowTypeError;
exports.shimConnectionState = shimConnectionState;
exports.removeAllowExtmapMixed = removeAllowExtmapMixed;

var _sdp = require('sdp');

var _sdp2 = _interopRequireDefault(_sdp);

var _utils = require('./utils');

var utils = _interopRequireWildcard(_utils);

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function shimRTCIceCandidate(window) {
  // foundation is arbitrarily chosen as an indicator for full support for
  // https://w3c.github.io/webrtc-pc/#rtcicecandidate-interface
  if (!window.RTCIceCandidate || window.RTCIceCandidate && 'foundation' in window.RTCIceCandidate.prototype) {
    return;
  }

  var NativeRTCIceCandidate = window.RTCIceCandidate;
  window.RTCIceCandidate = function RTCIceCandidate(args) {
    // Remove the a= which shouldn't be part of the candidate string.
    if ((typeof args === 'undefined' ? 'undefined' : _typeof(args)) === 'object' && args.candidate && args.candidate.indexOf('a=') === 0) {
      args = JSON.parse(JSON.stringify(args));
      args.candidate = args.candidate.substr(2);
    }

    if (args.candidate && args.candidate.length) {
      // Augment the native candidate with the parsed fields.
      var nativeCandidate = new NativeRTCIceCandidate(args);
      var parsedCandidate = _sdp2.default.parseCandidate(args.candidate);
      var augmentedCandidate = Object.assign(nativeCandidate, parsedCandidate);

      // Add a serializer that does not serialize the extra attributes.
      augmentedCandidate.toJSON = function toJSON() {
        return {
          candidate: augmentedCandidate.candidate,
          sdpMid: augmentedCandidate.sdpMid,
          sdpMLineIndex: augmentedCandidate.sdpMLineIndex,
          usernameFragment: augmentedCandidate.usernameFragment
        };
      };
      return augmentedCandidate;
    }
    return new NativeRTCIceCandidate(args);
  };
  window.RTCIceCandidate.prototype = NativeRTCIceCandidate.prototype;

  // Hook up the augmented candidate in onicecandidate and
  // addEventListener('icecandidate', ...)
  utils.wrapPeerConnectionEvent(window, 'icecandidate', function (e) {
    if (e.candidate) {
      Object.defineProperty(e, 'candidate', {
        value: new window.RTCIceCandidate(e.candidate),
        writable: 'false'
      });
    }
    return e;
  });
}

function shimMaxMessageSize(window) {
  if (!window.RTCPeerConnection) {
    return;
  }
  var browserDetails = utils.detectBrowser(window);

  if (!('sctp' in window.RTCPeerConnection.prototype)) {
    Object.defineProperty(window.RTCPeerConnection.prototype, 'sctp', {
      get: function get() {
        return typeof this._sctp === 'undefined' ? null : this._sctp;
      }
    });
  }

  var sctpInDescription = function sctpInDescription(description) {
    if (!description || !description.sdp) {
      return false;
    }
    var sections = _sdp2.default.splitSections(description.sdp);
    sections.shift();
    return sections.some(function (mediaSection) {
      var mLine = _sdp2.default.parseMLine(mediaSection);
      return mLine && mLine.kind === 'application' && mLine.protocol.indexOf('SCTP') !== -1;
    });
  };

  var getRemoteFirefoxVersion = function getRemoteFirefoxVersion(description) {
    // TODO: Is there a better solution for detecting Firefox?
    var match = description.sdp.match(/mozilla...THIS_IS_SDPARTA-(\d+)/);
    if (match === null || match.length < 2) {
      return -1;
    }
    var version = parseInt(match[1], 10);
    // Test for NaN (yes, this is ugly)
    return version !== version ? -1 : version;
  };

  var getCanSendMaxMessageSize = function getCanSendMaxMessageSize(remoteIsFirefox) {
    // Every implementation we know can send at least 64 KiB.
    // Note: Although Chrome is technically able to send up to 256 KiB, the
    //       data does not reach the other peer reliably.
    //       See: https://bugs.chromium.org/p/webrtc/issues/detail?id=8419
    var canSendMaxMessageSize = 65536;
    if (browserDetails.browser === 'firefox') {
      if (browserDetails.version < 57) {
        if (remoteIsFirefox === -1) {
          // FF < 57 will send in 16 KiB chunks using the deprecated PPID
          // fragmentation.
          canSendMaxMessageSize = 16384;
        } else {
          // However, other FF (and RAWRTC) can reassemble PPID-fragmented
          // messages. Thus, supporting ~2 GiB when sending.
          canSendMaxMessageSize = 2147483637;
        }
      } else if (browserDetails.version < 60) {
        // Currently, all FF >= 57 will reset the remote maximum message size
        // to the default value when a data channel is created at a later
        // stage. :(
        // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831
        canSendMaxMessageSize = browserDetails.version === 57 ? 65535 : 65536;
      } else {
        // FF >= 60 supports sending ~2 GiB
        canSendMaxMessageSize = 2147483637;
      }
    }
    return canSendMaxMessageSize;
  };

  var getMaxMessageSize = function getMaxMessageSize(description, remoteIsFirefox) {
    // Note: 65536 bytes is the default value from the SDP spec. Also,
    //       every implementation we know supports receiving 65536 bytes.
    var maxMessageSize = 65536;

    // FF 57 has a slightly incorrect default remote max message size, so
    // we need to adjust it here to avoid a failure when sending.
    // See: https://bugzilla.mozilla.org/show_bug.cgi?id=1425697
    if (browserDetails.browser === 'firefox' && browserDetails.version === 57) {
      maxMessageSize = 65535;
    }

    var match = _sdp2.default.matchPrefix(description.sdp, 'a=max-message-size:');
    if (match.length > 0) {
      maxMessageSize = parseInt(match[0].substr(19), 10);
    } else if (browserDetails.browser === 'firefox' && remoteIsFirefox !== -1) {
      // If the maximum message size is not present in the remote SDP and
      // both local and remote are Firefox, the remote peer can receive
      // ~2 GiB.
      maxMessageSize = 2147483637;
    }
    return maxMessageSize;
  };

  var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription;
  window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() {
    this._sctp = null;
    // Chrome decided to not expose .sctp in plan-b mode.
    // As usual, adapter.js has to do an 'ugly worakaround'
    // to cover up the mess.
    if (browserDetails.browser === 'chrome' && browserDetails.version >= 76) {
      var _getConfiguration = this.getConfiguration(),
          sdpSemantics = _getConfiguration.sdpSemantics;

      if (sdpSemantics === 'plan-b') {
        Object.defineProperty(this, 'sctp', {
          get: function get() {
            return typeof this._sctp === 'undefined' ? null : this._sctp;
          },

          enumerable: true,
          configurable: true
        });
      }
    }

    if (sctpInDescription(arguments[0])) {
      // Check if the remote is FF.
      var isFirefox = getRemoteFirefoxVersion(arguments[0]);

      // Get the maximum message size the local peer is capable of sending
      var canSendMMS = getCanSendMaxMessageSize(isFirefox);

      // Get the maximum message size of the remote peer.
      var remoteMMS = getMaxMessageSize(arguments[0], isFirefox);

      // Determine final maximum message size
      var maxMessageSize = void 0;
      if (canSendMMS === 0 && remoteMMS === 0) {
        maxMessageSize = Number.POSITIVE_INFINITY;
      } else if (canSendMMS === 0 || remoteMMS === 0) {
        maxMessageSize = Math.max(canSendMMS, remoteMMS);
      } else {
        maxMessageSize = Math.min(canSendMMS, remoteMMS);
      }

      // Create a dummy RTCSctpTransport object and the 'maxMessageSize'
      // attribute.
      var sctp = {};
      Object.defineProperty(sctp, 'maxMessageSize', {
        get: function get() {
          return maxMessageSize;
        }
      });
      this._sctp = sctp;
    }

    return origSetRemoteDescription.apply(this, arguments);
  };
}

function shimSendThrowTypeError(window) {
  if (!(window.RTCPeerConnection && 'createDataChannel' in window.RTCPeerConnection.prototype)) {
    return;
  }

  // Note: Although Firefox >= 57 has a native implementation, the maximum
  //       message size can be reset for all data channels at a later stage.
  //       See: https://bugzilla.mozilla.org/show_bug.cgi?id=1426831

  function wrapDcSend(dc, pc) {
    var origDataChannelSend = dc.send;
    dc.send = function send() {
      var data = arguments[0];
      var length = data.length || data.size || data.byteLength;
      if (dc.readyState === 'open' && pc.sctp && length > pc.sctp.maxMessageSize) {
        throw new TypeError('Message too large (can send a maximum of ' + pc.sctp.maxMessageSize + ' bytes)');
      }
      return origDataChannelSend.apply(dc, arguments);
    };
  }
  var origCreateDataChannel = window.RTCPeerConnection.prototype.createDataChannel;
  window.RTCPeerConnection.prototype.createDataChannel = function createDataChannel() {
    var dataChannel = origCreateDataChannel.apply(this, arguments);
    wrapDcSend(dataChannel, this);
    return dataChannel;
  };
  utils.wrapPeerConnectionEvent(window, 'datachannel', function (e) {
    wrapDcSend(e.channel, e.target);
    return e;
  });
}

/* shims RTCConnectionState by pretending it is the same as iceConnectionState.
 * See https://bugs.chromium.org/p/webrtc/issues/detail?id=6145#c12
 * for why this is a valid hack in Chrome. In Firefox it is slightly incorrect
 * since DTLS failures would be hidden. See
 * https://bugzilla.mozilla.org/show_bug.cgi?id=1265827
 * for the Firefox tracking bug.
 */
function shimConnectionState(window) {
  if (!window.RTCPeerConnection || 'connectionState' in window.RTCPeerConnection.prototype) {
    return;
  }
  var proto = window.RTCPeerConnection.prototype;
  Object.defineProperty(proto, 'connectionState', {
    get: function get() {
      return {
        completed: 'connected',
        checking: 'connecting'
      }[this.iceConnectionState] || this.iceConnectionState;
    },

    enumerable: true,
    configurable: true
  });
  Object.defineProperty(proto, 'onconnectionstatechange', {
    get: function get() {
      return this._onconnectionstatechange || null;
    },
    set: function set(cb) {
      if (this._onconnectionstatechange) {
        this.removeEventListener('connectionstatechange', this._onconnectionstatechange);
        delete this._onconnectionstatechange;
      }
      if (cb) {
        this.addEventListener('connectionstatechange', this._onconnectionstatechange = cb);
      }
    },

    enumerable: true,
    configurable: true
  });

  ['setLocalDescription', 'setRemoteDescription'].forEach(function (method) {
    var origMethod = proto[method];
    proto[method] = function () {
      if (!this._connectionstatechangepoly) {
        this._connectionstatechangepoly = function (e) {
          var pc = e.target;
          if (pc._lastConnectionState !== pc.connectionState) {
            pc._lastConnectionState = pc.connectionState;
            var newEvent = new Event('connectionstatechange', e);
            pc.dispatchEvent(newEvent);
          }
          return e;
        };
        this.addEventListener('iceconnectionstatechange', this._connectionstatechangepoly);
      }
      return origMethod.apply(this, arguments);
    };
  });
}

function removeAllowExtmapMixed(window) {
  /* remove a=extmap-allow-mixed for Chrome < M71 */
  if (!window.RTCPeerConnection) {
    return;
  }
  var browserDetails = utils.detectBrowser(window);
  if (browserDetails.browser === 'chrome' && browserDetails.version >= 71) {
    return;
  }
  var nativeSRD = window.RTCPeerConnection.prototype.setRemoteDescription;
  window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription(desc) {
    if (desc && desc.sdp && desc.sdp.indexOf('\na=extmap-allow-mixed') !== -1) {
      desc.sdp = desc.sdp.split('\n').filter(function (line) {
        return line.trim() !== 'a=extmap-allow-mixed';
      }).join('\n');
    }
    return nativeSRD.apply(this, arguments);
  };
}

},{"./utils":15,"sdp":17}],7:[function(require,module,exports){
/*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.shimGetDisplayMedia = exports.shimGetUserMedia = undefined;

var _getusermedia = require('./getusermedia');

Object.defineProperty(exports, 'shimGetUserMedia', {
  enumerable: true,
  get: function get() {
    return _getusermedia.shimGetUserMedia;
  }
});

var _getdisplaymedia = require('./getdisplaymedia');

Object.defineProperty(exports, 'shimGetDisplayMedia', {
  enumerable: true,
  get: function get() {
    return _getdisplaymedia.shimGetDisplayMedia;
  }
});
exports.shimPeerConnection = shimPeerConnection;
exports.shimReplaceTrack = shimReplaceTrack;

var _utils = require('../utils');

var utils = _interopRequireWildcard(_utils);

var _filtericeservers = require('./filtericeservers');

var _rtcpeerconnectionShim = require('rtcpeerconnection-shim');

var _rtcpeerconnectionShim2 = _interopRequireDefault(_rtcpeerconnectionShim);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

function shimPeerConnection(window) {
  var browserDetails = utils.detectBrowser(window);

  if (window.RTCIceGatherer) {
    if (!window.RTCIceCandidate) {
      window.RTCIceCandidate = function RTCIceCandidate(args) {
        return args;
      };
    }
    if (!window.RTCSessionDescription) {
      window.RTCSessionDescription = function RTCSessionDescription(args) {
        return args;
      };
    }
    // this adds an additional event listener to MediaStrackTrack that signals
    // when a tracks enabled property was changed. Workaround for a bug in
    // addStream, see below. No longer required in 15025+
    if (browserDetails.version < 15025) {
      var origMSTEnabled = Object.getOwnPropertyDescriptor(window.MediaStreamTrack.prototype, 'enabled');
      Object.defineProperty(window.MediaStreamTrack.prototype, 'enabled', {
        set: function set(value) {
          origMSTEnabled.set.call(this, value);
          var ev = new Event('enabled');
          ev.enabled = value;
          this.dispatchEvent(ev);
        }
      });
    }
  }

  // ORTC defines the DTMF sender a bit different.
  // https://github.com/w3c/ortc/issues/714
  if (window.RTCRtpSender && !('dtmf' in window.RTCRtpSender.prototype)) {
    Object.defineProperty(window.RTCRtpSender.prototype, 'dtmf', {
      get: function get() {
        if (this._dtmf === undefined) {
          if (this.track.kind === 'audio') {
            this._dtmf = new window.RTCDtmfSender(this);
          } else if (this.track.kind === 'video') {
            this._dtmf = null;
          }
        }
        return this._dtmf;
      }
    });
  }
  // Edge currently only implements the RTCDtmfSender, not the
  // RTCDTMFSender alias. See http://draft.ortc.org/#rtcdtmfsender2*
  if (window.RTCDtmfSender && !window.RTCDTMFSender) {
    window.RTCDTMFSender = window.RTCDtmfSender;
  }

  var RTCPeerConnectionShim = (0, _rtcpeerconnectionShim2.default)(window, browserDetails.version);
  window.RTCPeerConnection = function RTCPeerConnection(config) {
    if (config && config.iceServers) {
      config.iceServers = (0, _filtericeservers.filterIceServers)(config.iceServers, browserDetails.version);
      utils.log('ICE servers after filtering:', config.iceServers);
    }
    return new RTCPeerConnectionShim(config);
  };
  window.RTCPeerConnection.prototype = RTCPeerConnectionShim.prototype;
}

function shimReplaceTrack(window) {
  // ORTC has replaceTrack -- https://github.com/w3c/ortc/issues/614
  if (window.RTCRtpSender && !('replaceTrack' in window.RTCRtpSender.prototype)) {
    window.RTCRtpSender.prototype.replaceTrack = window.RTCRtpSender.prototype.setTrack;
  }
}

},{"../utils":15,"./filtericeservers":8,"./getdisplaymedia":9,"./getusermedia":10,"rtcpeerconnection-shim":16}],8:[function(require,module,exports){
/*
 *  Copyright (c) 2018 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.filterIceServers = filterIceServers;

var _utils = require('../utils');

var utils = _interopRequireWildcard(_utils);

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

// Edge does not like
// 1) stun: filtered after 14393 unless ?transport=udp is present
// 2) turn: that does not have all of turn:host:port?transport=udp
// 3) turn: with ipv6 addresses
// 4) turn: occurring muliple times
function filterIceServers(iceServers, edgeVersion) {
  var hasTurn = false;
  iceServers = JSON.parse(JSON.stringify(iceServers));
  return iceServers.filter(function (server) {
    if (server && (server.urls || server.url)) {
      var urls = server.urls || server.url;
      if (server.url && !server.urls) {
        utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
      }
      var isString = typeof urls === 'string';
      if (isString) {
        urls = [urls];
      }
      urls = urls.filter(function (url) {
        // filter STUN unconditionally.
        if (url.indexOf('stun:') === 0) {
          return false;
        }

        var validTurn = url.startsWith('turn') && !url.startsWith('turn:[') && url.includes('transport=udp');
        if (validTurn && !hasTurn) {
          hasTurn = true;
          return true;
        }
        return validTurn && !hasTurn;
      });

      delete server.url;
      server.urls = isString ? urls[0] : urls;
      return !!urls.length;
    }
  });
}

},{"../utils":15}],9:[function(require,module,exports){
/*
 *  Copyright (c) 2018 The adapter.js project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.shimGetDisplayMedia = shimGetDisplayMedia;
function shimGetDisplayMedia(window) {
  if (!('getDisplayMedia' in window.navigator)) {
    return;
  }
  if (!window.navigator.mediaDevices) {
    return;
  }
  if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) {
    return;
  }
  window.navigator.mediaDevices.getDisplayMedia = window.navigator.getDisplayMedia.bind(window.navigator);
}

},{}],10:[function(require,module,exports){
/*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.shimGetUserMedia = shimGetUserMedia;
function shimGetUserMedia(window) {
  var navigator = window && window.navigator;

  var shimError_ = function shimError_(e) {
    return {
      name: { PermissionDeniedError: 'NotAllowedError' }[e.name] || e.name,
      message: e.message,
      constraint: e.constraint,
      toString: function toString() {
        return this.name;
      }
    };
  };

  // getUserMedia error shim.
  var origGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
  navigator.mediaDevices.getUserMedia = function (c) {
    return origGetUserMedia(c).catch(function (e) {
      return Promise.reject(shimError_(e));
    });
  };
}

},{}],11:[function(require,module,exports){
/*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.shimGetDisplayMedia = exports.shimGetUserMedia = undefined;

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

var _getusermedia = require('./getusermedia');

Object.defineProperty(exports, 'shimGetUserMedia', {
  enumerable: true,
  get: function get() {
    return _getusermedia.shimGetUserMedia;
  }
});

var _getdisplaymedia = require('./getdisplaymedia');

Object.defineProperty(exports, 'shimGetDisplayMedia', {
  enumerable: true,
  get: function get() {
    return _getdisplaymedia.shimGetDisplayMedia;
  }
});
exports.shimOnTrack = shimOnTrack;
exports.shimPeerConnection = shimPeerConnection;
exports.shimSenderGetStats = shimSenderGetStats;
exports.shimReceiverGetStats = shimReceiverGetStats;
exports.shimRemoveStream = shimRemoveStream;
exports.shimRTCDataChannel = shimRTCDataChannel;
exports.shimAddTransceiver = shimAddTransceiver;
exports.shimCreateOffer = shimCreateOffer;
exports.shimCreateAnswer = shimCreateAnswer;

var _utils = require('../utils');

var utils = _interopRequireWildcard(_utils);

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

function shimOnTrack(window) {
  if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCTrackEvent && 'receiver' in window.RTCTrackEvent.prototype && !('transceiver' in window.RTCTrackEvent.prototype)) {
    Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
      get: function get() {
        return { receiver: this.receiver };
      }
    });
  }
}

function shimPeerConnection(window) {
  var browserDetails = utils.detectBrowser(window);

  if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) !== 'object' || !(window.RTCPeerConnection || window.mozRTCPeerConnection)) {
    return; // probably media.peerconnection.enabled=false in about:config
  }
  if (!window.RTCPeerConnection && window.mozRTCPeerConnection) {
    // very basic support for old versions.
    window.RTCPeerConnection = window.mozRTCPeerConnection;
  }

  if (browserDetails.version < 53) {
    // shim away need for obsolete RTCIceCandidate/RTCSessionDescription.
    ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'].forEach(function (method) {
      var nativeMethod = window.RTCPeerConnection.prototype[method];
      var methodObj = _defineProperty({}, method, function () {
        arguments[0] = new (method === 'addIceCandidate' ? window.RTCIceCandidate : window.RTCSessionDescription)(arguments[0]);
        return nativeMethod.apply(this, arguments);
      });
      window.RTCPeerConnection.prototype[method] = methodObj[method];
    });
  }

  // support for addIceCandidate(null or undefined)
  // as well as ignoring {sdpMid, candidate: ""}
  if (browserDetails.version < 68) {
    var nativeAddIceCandidate = window.RTCPeerConnection.prototype.addIceCandidate;
    window.RTCPeerConnection.prototype.addIceCandidate = function addIceCandidate() {
      if (!arguments[0]) {
        if (arguments[1]) {
          arguments[1].apply(null);
        }
        return Promise.resolve();
      }
      // Firefox 68+ emits and processes {candidate: "", ...}, ignore
      // in older versions.
      if (arguments[0] && arguments[0].candidate === '') {
        return Promise.resolve();
      }
      return nativeAddIceCandidate.apply(this, arguments);
    };
  }

  var modernStatsTypes = {
    inboundrtp: 'inbound-rtp',
    outboundrtp: 'outbound-rtp',
    candidatepair: 'candidate-pair',
    localcandidate: 'local-candidate',
    remotecandidate: 'remote-candidate'
  };

  var nativeGetStats = window.RTCPeerConnection.prototype.getStats;
  window.RTCPeerConnection.prototype.getStats = function getStats() {
    var _arguments = Array.prototype.slice.call(arguments),
        selector = _arguments[0],
        onSucc = _arguments[1],
        onErr = _arguments[2];

    return nativeGetStats.apply(this, [selector || null]).then(function (stats) {
      if (browserDetails.version < 53 && !onSucc) {
        // Shim only promise getStats with spec-hyphens in type names
        // Leave callback version alone; misc old uses of forEach before Map
        try {
          stats.forEach(function (stat) {
            stat.type = modernStatsTypes[stat.type] || stat.type;
          });
        } catch (e) {
          if (e.name !== 'TypeError') {
            throw e;
          }
          // Avoid TypeError: "type" is read-only, in old versions. 34-43ish
          stats.forEach(function (stat, i) {
            stats.set(i, Object.assign({}, stat, {
              type: modernStatsTypes[stat.type] || stat.type
            }));
          });
        }
      }
      return stats;
    }).then(onSucc, onErr);
  };
}

function shimSenderGetStats(window) {
  if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) {
    return;
  }
  if (window.RTCRtpSender && 'getStats' in window.RTCRtpSender.prototype) {
    return;
  }
  var origGetSenders = window.RTCPeerConnection.prototype.getSenders;
  if (origGetSenders) {
    window.RTCPeerConnection.prototype.getSenders = function getSenders() {
      var _this = this;

      var senders = origGetSenders.apply(this, []);
      senders.forEach(function (sender) {
        return sender._pc = _this;
      });
      return senders;
    };
  }

  var origAddTrack = window.RTCPeerConnection.prototype.addTrack;
  if (origAddTrack) {
    window.RTCPeerConnection.prototype.addTrack = function addTrack() {
      var sender = origAddTrack.apply(this, arguments);
      sender._pc = this;
      return sender;
    };
  }
  window.RTCRtpSender.prototype.getStats = function getStats() {
    return this.track ? this._pc.getStats(this.track) : Promise.resolve(new Map());
  };
}

function shimReceiverGetStats(window) {
  if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection && window.RTCRtpSender)) {
    return;
  }
  if (window.RTCRtpSender && 'getStats' in window.RTCRtpReceiver.prototype) {
    return;
  }
  var origGetReceivers = window.RTCPeerConnection.prototype.getReceivers;
  if (origGetReceivers) {
    window.RTCPeerConnection.prototype.getReceivers = function getReceivers() {
      var _this2 = this;

      var receivers = origGetReceivers.apply(this, []);
      receivers.forEach(function (receiver) {
        return receiver._pc = _this2;
      });
      return receivers;
    };
  }
  utils.wrapPeerConnectionEvent(window, 'track', function (e) {
    e.receiver._pc = e.srcElement;
    return e;
  });
  window.RTCRtpReceiver.prototype.getStats = function getStats() {
    return this._pc.getStats(this.track);
  };
}

function shimRemoveStream(window) {
  if (!window.RTCPeerConnection || 'removeStream' in window.RTCPeerConnection.prototype) {
    return;
  }
  window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) {
    var _this3 = this;

    utils.deprecated('removeStream', 'removeTrack');
    this.getSenders().forEach(function (sender) {
      if (sender.track && stream.getTracks().includes(sender.track)) {
        _this3.removeTrack(sender);
      }
    });
  };
}

function shimRTCDataChannel(window) {
  // rename DataChannel to RTCDataChannel (native fix in FF60):
  // https://bugzilla.mozilla.org/show_bug.cgi?id=1173851
  if (window.DataChannel && !window.RTCDataChannel) {
    window.RTCDataChannel = window.DataChannel;
  }
}

function shimAddTransceiver(window) {
  // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
  // Firefox ignores the init sendEncodings options passed to addTransceiver
  // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
  if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection)) {
    return;
  }
  var origAddTransceiver = window.RTCPeerConnection.prototype.addTransceiver;
  if (origAddTransceiver) {
    window.RTCPeerConnection.prototype.addTransceiver = function addTransceiver() {
      this.setParametersPromises = [];
      var initParameters = arguments[1];
      var shouldPerformCheck = initParameters && 'sendEncodings' in initParameters;
      if (shouldPerformCheck) {
        // If sendEncodings params are provided, validate grammar
        initParameters.sendEncodings.forEach(function (encodingParam) {
          if ('rid' in encodingParam) {
            var ridRegex = /^[a-z0-9]{0,16}$/i;
            if (!ridRegex.test(encodingParam.rid)) {
              throw new TypeError('Invalid RID value provided.');
            }
          }
          if ('scaleResolutionDownBy' in encodingParam) {
            if (!(parseFloat(encodingParam.scaleResolutionDownBy) >= 1.0)) {
              throw new RangeError('scale_resolution_down_by must be >= 1.0');
            }
          }
          if ('maxFramerate' in encodingParam) {
            if (!(parseFloat(encodingParam.maxFramerate) >= 0)) {
              throw new RangeError('max_framerate must be >= 0.0');
            }
          }
        });
      }
      var transceiver = origAddTransceiver.apply(this, arguments);
      if (shouldPerformCheck) {
        // Check if the init options were applied. If not we do this in an
        // asynchronous way and save the promise reference in a global object.
        // This is an ugly hack, but at the same time is way more robust than
        // checking the sender parameters before and after the createOffer
        // Also note that after the createoffer we are not 100% sure that
        // the params were asynchronously applied so we might miss the
        // opportunity to recreate offer.
        var sender = transceiver.sender;

        var params = sender.getParameters();
        if (!('encodings' in params)) {
          params.encodings = initParameters.sendEncodings;
          this.setParametersPromises.push(sender.setParameters(params).catch(function () {}));
        }
      }
      return transceiver;
    };
  }
}

function shimCreateOffer(window) {
  // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
  // Firefox ignores the init sendEncodings options passed to addTransceiver
  // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
  if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection)) {
    return;
  }
  var origCreateOffer = window.RTCPeerConnection.prototype.createOffer;
  window.RTCPeerConnection.prototype.createOffer = function createOffer() {
    var _this4 = this,
        _arguments2 = arguments;

    if (this.setParametersPromises && this.setParametersPromises.length) {
      return Promise.all(this.setParametersPromises).then(function () {
        return origCreateOffer.apply(_this4, _arguments2);
      }).finally(function () {
        _this4.setParametersPromises = [];
      });
    }
    return origCreateOffer.apply(this, arguments);
  };
}

function shimCreateAnswer(window) {
  // https://github.com/webrtcHacks/adapter/issues/998#issuecomment-516921647
  // Firefox ignores the init sendEncodings options passed to addTransceiver
  // https://bugzilla.mozilla.org/show_bug.cgi?id=1396918
  if (!((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCPeerConnection)) {
    return;
  }
  var origCreateAnswer = window.RTCPeerConnection.prototype.createAnswer;
  window.RTCPeerConnection.prototype.createAnswer = function createAnswer() {
    var _this5 = this,
        _arguments3 = arguments;

    if (this.setParametersPromises && this.setParametersPromises.length) {
      return Promise.all(this.setParametersPromises).then(function () {
        return origCreateAnswer.apply(_this5, _arguments3);
      }).finally(function () {
        _this5.setParametersPromises = [];
      });
    }
    return origCreateAnswer.apply(this, arguments);
  };
}

},{"../utils":15,"./getdisplaymedia":12,"./getusermedia":13}],12:[function(require,module,exports){
/*
 *  Copyright (c) 2018 The adapter.js project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.shimGetDisplayMedia = shimGetDisplayMedia;
function shimGetDisplayMedia(window, preferredMediaSource) {
  if (window.navigator.mediaDevices && 'getDisplayMedia' in window.navigator.mediaDevices) {
    return;
  }
  if (!window.navigator.mediaDevices) {
    return;
  }
  window.navigator.mediaDevices.getDisplayMedia = function getDisplayMedia(constraints) {
    if (!(constraints && constraints.video)) {
      var err = new DOMException('getDisplayMedia without video ' + 'constraints is undefined');
      err.name = 'NotFoundError';
      // from https://heycam.github.io/webidl/#idl-DOMException-error-names
      err.code = 8;
      return Promise.reject(err);
    }
    if (constraints.video === true) {
      constraints.video = { mediaSource: preferredMediaSource };
    } else {
      constraints.video.mediaSource = preferredMediaSource;
    }
    return window.navigator.mediaDevices.getUserMedia(constraints);
  };
}

},{}],13:[function(require,module,exports){
/*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

exports.shimGetUserMedia = shimGetUserMedia;

var _utils = require('../utils');

var utils = _interopRequireWildcard(_utils);

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

function shimGetUserMedia(window) {
  var browserDetails = utils.detectBrowser(window);
  var navigator = window && window.navigator;
  var MediaStreamTrack = window && window.MediaStreamTrack;

  navigator.getUserMedia = function (constraints, onSuccess, onError) {
    // Replace Firefox 44+'s deprecation warning with unprefixed version.
    utils.deprecated('navigator.getUserMedia', 'navigator.mediaDevices.getUserMedia');
    navigator.mediaDevices.getUserMedia(constraints).then(onSuccess, onError);
  };

  if (!(browserDetails.version > 55 && 'autoGainControl' in navigator.mediaDevices.getSupportedConstraints())) {
    var remap = function remap(obj, a, b) {
      if (a in obj && !(b in obj)) {
        obj[b] = obj[a];
        delete obj[a];
      }
    };

    var nativeGetUserMedia = navigator.mediaDevices.getUserMedia.bind(navigator.mediaDevices);
    navigator.mediaDevices.getUserMedia = function (c) {
      if ((typeof c === 'undefined' ? 'undefined' : _typeof(c)) === 'object' && _typeof(c.audio) === 'object') {
        c = JSON.parse(JSON.stringify(c));
        remap(c.audio, 'autoGainControl', 'mozAutoGainControl');
        remap(c.audio, 'noiseSuppression', 'mozNoiseSuppression');
      }
      return nativeGetUserMedia(c);
    };

    if (MediaStreamTrack && MediaStreamTrack.prototype.getSettings) {
      var nativeGetSettings = MediaStreamTrack.prototype.getSettings;
      MediaStreamTrack.prototype.getSettings = function () {
        var obj = nativeGetSettings.apply(this, arguments);
        remap(obj, 'mozAutoGainControl', 'autoGainControl');
        remap(obj, 'mozNoiseSuppression', 'noiseSuppression');
        return obj;
      };
    }

    if (MediaStreamTrack && MediaStreamTrack.prototype.applyConstraints) {
      var nativeApplyConstraints = MediaStreamTrack.prototype.applyConstraints;
      MediaStreamTrack.prototype.applyConstraints = function (c) {
        if (this.kind === 'audio' && (typeof c === 'undefined' ? 'undefined' : _typeof(c)) === 'object') {
          c = JSON.parse(JSON.stringify(c));
          remap(c, 'autoGainControl', 'mozAutoGainControl');
          remap(c, 'noiseSuppression', 'mozNoiseSuppression');
        }
        return nativeApplyConstraints.apply(this, [c]);
      };
    }
  }
}

},{"../utils":15}],14:[function(require,module,exports){
/*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

exports.shimLocalStreamsAPI = shimLocalStreamsAPI;
exports.shimRemoteStreamsAPI = shimRemoteStreamsAPI;
exports.shimCallbacksAPI = shimCallbacksAPI;
exports.shimGetUserMedia = shimGetUserMedia;
exports.shimConstraints = shimConstraints;
exports.shimRTCIceServerUrls = shimRTCIceServerUrls;
exports.shimTrackEventTransceiver = shimTrackEventTransceiver;
exports.shimCreateOfferLegacy = shimCreateOfferLegacy;

var _utils = require('../utils');

var utils = _interopRequireWildcard(_utils);

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) newObj[key] = obj[key]; } } newObj.default = obj; return newObj; } }

function shimLocalStreamsAPI(window) {
  if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) !== 'object' || !window.RTCPeerConnection) {
    return;
  }
  if (!('getLocalStreams' in window.RTCPeerConnection.prototype)) {
    window.RTCPeerConnection.prototype.getLocalStreams = function getLocalStreams() {
      if (!this._localStreams) {
        this._localStreams = [];
      }
      return this._localStreams;
    };
  }
  if (!('addStream' in window.RTCPeerConnection.prototype)) {
    var _addTrack = window.RTCPeerConnection.prototype.addTrack;
    window.RTCPeerConnection.prototype.addStream = function addStream(stream) {
      var _this = this;

      if (!this._localStreams) {
        this._localStreams = [];
      }
      if (!this._localStreams.includes(stream)) {
        this._localStreams.push(stream);
      }
      // Try to emulate Chrome's behaviour of adding in audio-video order.
      // Safari orders by track id.
      stream.getAudioTracks().forEach(function (track) {
        return _addTrack.call(_this, track, stream);
      });
      stream.getVideoTracks().forEach(function (track) {
        return _addTrack.call(_this, track, stream);
      });
    };

    window.RTCPeerConnection.prototype.addTrack = function addTrack(track) {
      var stream = arguments[1];
      if (stream) {
        if (!this._localStreams) {
          this._localStreams = [stream];
        } else if (!this._localStreams.includes(stream)) {
          this._localStreams.push(stream);
        }
      }
      return _addTrack.apply(this, arguments);
    };
  }
  if (!('removeStream' in window.RTCPeerConnection.prototype)) {
    window.RTCPeerConnection.prototype.removeStream = function removeStream(stream) {
      var _this2 = this;

      if (!this._localStreams) {
        this._localStreams = [];
      }
      var index = this._localStreams.indexOf(stream);
      if (index === -1) {
        return;
      }
      this._localStreams.splice(index, 1);
      var tracks = stream.getTracks();
      this.getSenders().forEach(function (sender) {
        if (tracks.includes(sender.track)) {
          _this2.removeTrack(sender);
        }
      });
    };
  }
}

function shimRemoteStreamsAPI(window) {
  if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) !== 'object' || !window.RTCPeerConnection) {
    return;
  }
  if (!('getRemoteStreams' in window.RTCPeerConnection.prototype)) {
    window.RTCPeerConnection.prototype.getRemoteStreams = function getRemoteStreams() {
      return this._remoteStreams ? this._remoteStreams : [];
    };
  }
  if (!('onaddstream' in window.RTCPeerConnection.prototype)) {
    Object.defineProperty(window.RTCPeerConnection.prototype, 'onaddstream', {
      get: function get() {
        return this._onaddstream;
      },
      set: function set(f) {
        var _this3 = this;

        if (this._onaddstream) {
          this.removeEventListener('addstream', this._onaddstream);
          this.removeEventListener('track', this._onaddstreampoly);
        }
        this.addEventListener('addstream', this._onaddstream = f);
        this.addEventListener('track', this._onaddstreampoly = function (e) {
          e.streams.forEach(function (stream) {
            if (!_this3._remoteStreams) {
              _this3._remoteStreams = [];
            }
            if (_this3._remoteStreams.includes(stream)) {
              return;
            }
            _this3._remoteStreams.push(stream);
            var event = new Event('addstream');
            event.stream = stream;
            _this3.dispatchEvent(event);
          });
        });
      }
    });
    var origSetRemoteDescription = window.RTCPeerConnection.prototype.setRemoteDescription;
    window.RTCPeerConnection.prototype.setRemoteDescription = function setRemoteDescription() {
      var pc = this;
      if (!this._onaddstreampoly) {
        this.addEventListener('track', this._onaddstreampoly = function (e) {
          e.streams.forEach(function (stream) {
            if (!pc._remoteStreams) {
              pc._remoteStreams = [];
            }
            if (pc._remoteStreams.indexOf(stream) >= 0) {
              return;
            }
            pc._remoteStreams.push(stream);
            var event = new Event('addstream');
            event.stream = stream;
            pc.dispatchEvent(event);
          });
        });
      }
      return origSetRemoteDescription.apply(pc, arguments);
    };
  }
}

function shimCallbacksAPI(window) {
  if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) !== 'object' || !window.RTCPeerConnection) {
    return;
  }
  var prototype = window.RTCPeerConnection.prototype;
  var origCreateOffer = prototype.createOffer;
  var origCreateAnswer = prototype.createAnswer;
  var setLocalDescription = prototype.setLocalDescription;
  var setRemoteDescription = prototype.setRemoteDescription;
  var addIceCandidate = prototype.addIceCandidate;

  prototype.createOffer = function createOffer(successCallback, failureCallback) {
    var options = arguments.length >= 2 ? arguments[2] : arguments[0];
    var promise = origCreateOffer.apply(this, [options]);
    if (!failureCallback) {
      return promise;
    }
    promise.then(successCallback, failureCallback);
    return Promise.resolve();
  };

  prototype.createAnswer = function createAnswer(successCallback, failureCallback) {
    var options = arguments.length >= 2 ? arguments[2] : arguments[0];
    var promise = origCreateAnswer.apply(this, [options]);
    if (!failureCallback) {
      return promise;
    }
    promise.then(successCallback, failureCallback);
    return Promise.resolve();
  };

  var withCallback = function withCallback(description, successCallback, failureCallback) {
    var promise = setLocalDescription.apply(this, [description]);
    if (!failureCallback) {
      return promise;
    }
    promise.then(successCallback, failureCallback);
    return Promise.resolve();
  };
  prototype.setLocalDescription = withCallback;

  withCallback = function withCallback(description, successCallback, failureCallback) {
    var promise = setRemoteDescription.apply(this, [description]);
    if (!failureCallback) {
      return promise;
    }
    promise.then(successCallback, failureCallback);
    return Promise.resolve();
  };
  prototype.setRemoteDescription = withCallback;

  withCallback = function withCallback(candidate, successCallback, failureCallback) {
    var promise = addIceCandidate.apply(this, [candidate]);
    if (!failureCallback) {
      return promise;
    }
    promise.then(successCallback, failureCallback);
    return Promise.resolve();
  };
  prototype.addIceCandidate = withCallback;
}

function shimGetUserMedia(window) {
  var navigator = window && window.navigator;

  if (navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    // shim not needed in Safari 12.1
    var mediaDevices = navigator.mediaDevices;
    var _getUserMedia = mediaDevices.getUserMedia.bind(mediaDevices);
    navigator.mediaDevices.getUserMedia = function (constraints) {
      return _getUserMedia(shimConstraints(constraints));
    };
  }

  if (!navigator.getUserMedia && navigator.mediaDevices && navigator.mediaDevices.getUserMedia) {
    navigator.getUserMedia = function getUserMedia(constraints, cb, errcb) {
      navigator.mediaDevices.getUserMedia(constraints).then(cb, errcb);
    }.bind(navigator);
  }
}

function shimConstraints(constraints) {
  if (constraints && constraints.video !== undefined) {
    return Object.assign({}, constraints, { video: utils.compactObject(constraints.video) });
  }

  return constraints;
}

function shimRTCIceServerUrls(window) {
  // migrate from non-spec RTCIceServer.url to RTCIceServer.urls
  var OrigPeerConnection = window.RTCPeerConnection;
  window.RTCPeerConnection = function RTCPeerConnection(pcConfig, pcConstraints) {
    if (pcConfig && pcConfig.iceServers) {
      var newIceServers = [];
      for (var i = 0; i < pcConfig.iceServers.length; i++) {
        var server = pcConfig.iceServers[i];
        if (!server.hasOwnProperty('urls') && server.hasOwnProperty('url')) {
          utils.deprecated('RTCIceServer.url', 'RTCIceServer.urls');
          server = JSON.parse(JSON.stringify(server));
          server.urls = server.url;
          delete server.url;
          newIceServers.push(server);
        } else {
          newIceServers.push(pcConfig.iceServers[i]);
        }
      }
      pcConfig.iceServers = newIceServers;
    }
    return new OrigPeerConnection(pcConfig, pcConstraints);
  };
  window.RTCPeerConnection.prototype = OrigPeerConnection.prototype;
  // wrap static methods. Currently just generateCertificate.
  if ('generateCertificate' in window.RTCPeerConnection) {
    Object.defineProperty(window.RTCPeerConnection, 'generateCertificate', {
      get: function get() {
        return OrigPeerConnection.generateCertificate;
      }
    });
  }
}

function shimTrackEventTransceiver(window) {
  // Add event.transceiver member over deprecated event.receiver
  if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object' && window.RTCTrackEvent && 'receiver' in window.RTCTrackEvent.prototype && !('transceiver' in window.RTCTrackEvent.prototype)) {
    Object.defineProperty(window.RTCTrackEvent.prototype, 'transceiver', {
      get: function get() {
        return { receiver: this.receiver };
      }
    });
  }
}

function shimCreateOfferLegacy(window) {
  var origCreateOffer = window.RTCPeerConnection.prototype.createOffer;
  window.RTCPeerConnection.prototype.createOffer = function createOffer(offerOptions) {
    if (offerOptions) {
      if (typeof offerOptions.offerToReceiveAudio !== 'undefined') {
        // support bit values
        offerOptions.offerToReceiveAudio = !!offerOptions.offerToReceiveAudio;
      }
      var audioTransceiver = this.getTransceivers().find(function (transceiver) {
        return transceiver.receiver.track.kind === 'audio';
      });
      if (offerOptions.offerToReceiveAudio === false && audioTransceiver) {
        if (audioTransceiver.direction === 'sendrecv') {
          if (audioTransceiver.setDirection) {
            audioTransceiver.setDirection('sendonly');
          } else {
            audioTransceiver.direction = 'sendonly';
          }
        } else if (audioTransceiver.direction === 'recvonly') {
          if (audioTransceiver.setDirection) {
            audioTransceiver.setDirection('inactive');
          } else {
            audioTransceiver.direction = 'inactive';
          }
        }
      } else if (offerOptions.offerToReceiveAudio === true && !audioTransceiver) {
        this.addTransceiver('audio');
      }

      if (typeof offerOptions.offerToReceiveVideo !== 'undefined') {
        // support bit values
        offerOptions.offerToReceiveVideo = !!offerOptions.offerToReceiveVideo;
      }
      var videoTransceiver = this.getTransceivers().find(function (transceiver) {
        return transceiver.receiver.track.kind === 'video';
      });
      if (offerOptions.offerToReceiveVideo === false && videoTransceiver) {
        if (videoTransceiver.direction === 'sendrecv') {
          if (videoTransceiver.setDirection) {
            videoTransceiver.setDirection('sendonly');
          } else {
            videoTransceiver.direction = 'sendonly';
          }
        } else if (videoTransceiver.direction === 'recvonly') {
          if (videoTransceiver.setDirection) {
            videoTransceiver.setDirection('inactive');
          } else {
            videoTransceiver.direction = 'inactive';
          }
        }
      } else if (offerOptions.offerToReceiveVideo === true && !videoTransceiver) {
        this.addTransceiver('video');
      }
    }
    return origCreateOffer.apply(this, arguments);
  };
}

},{"../utils":15}],15:[function(require,module,exports){
/*
 *  Copyright (c) 2016 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
/* eslint-env node */
'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

exports.extractVersion = extractVersion;
exports.wrapPeerConnectionEvent = wrapPeerConnectionEvent;
exports.disableLog = disableLog;
exports.disableWarnings = disableWarnings;
exports.log = log;
exports.deprecated = deprecated;
exports.detectBrowser = detectBrowser;
exports.compactObject = compactObject;
exports.walkStats = walkStats;
exports.filterStats = filterStats;

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

var logDisabled_ = true;
var deprecationWarnings_ = true;

/**
 * Extract browser version out of the provided user agent string.
 *
 * @param {!string} uastring userAgent string.
 * @param {!string} expr Regular expression used as match criteria.
 * @param {!number} pos position in the version string to be returned.
 * @return {!number} browser version.
 */
function extractVersion(uastring, expr, pos) {
  var match = uastring.match(expr);
  return match && match.length >= pos && parseInt(match[pos], 10);
}

// Wraps the peerconnection event eventNameToWrap in a function
// which returns the modified event object (or false to prevent
// the event).
function wrapPeerConnectionEvent(window, eventNameToWrap, wrapper) {
  if (!window.RTCPeerConnection) {
    return;
  }
  var proto = window.RTCPeerConnection.prototype;
  var nativeAddEventListener = proto.addEventListener;
  proto.addEventListener = function (nativeEventName, cb) {
    if (nativeEventName !== eventNameToWrap) {
      return nativeAddEventListener.apply(this, arguments);
    }
    var wrappedCallback = function wrappedCallback(e) {
      var modifiedEvent = wrapper(e);
      if (modifiedEvent) {
        cb(modifiedEvent);
      }
    };
    this._eventMap = this._eventMap || {};
    this._eventMap[cb] = wrappedCallback;
    return nativeAddEventListener.apply(this, [nativeEventName, wrappedCallback]);
  };

  var nativeRemoveEventListener = proto.removeEventListener;
  proto.removeEventListener = function (nativeEventName, cb) {
    if (nativeEventName !== eventNameToWrap || !this._eventMap || !this._eventMap[cb]) {
      return nativeRemoveEventListener.apply(this, arguments);
    }
    var unwrappedCb = this._eventMap[cb];
    delete this._eventMap[cb];
    return nativeRemoveEventListener.apply(this, [nativeEventName, unwrappedCb]);
  };

  Object.defineProperty(proto, 'on' + eventNameToWrap, {
    get: function get() {
      return this['_on' + eventNameToWrap];
    },
    set: function set(cb) {
      if (this['_on' + eventNameToWrap]) {
        this.removeEventListener(eventNameToWrap, this['_on' + eventNameToWrap]);
        delete this['_on' + eventNameToWrap];
      }
      if (cb) {
        this.addEventListener(eventNameToWrap, this['_on' + eventNameToWrap] = cb);
      }
    },

    enumerable: true,
    configurable: true
  });
}

function disableLog(bool) {
  if (typeof bool !== 'boolean') {
    return new Error('Argument type: ' + (typeof bool === 'undefined' ? 'undefined' : _typeof(bool)) + '. Please use a boolean.');
  }
  logDisabled_ = bool;
  return bool ? 'adapter.js logging disabled' : 'adapter.js logging enabled';
}

/**
 * Disable or enable deprecation warnings
 * @param {!boolean} bool set to true to disable warnings.
 */
function disableWarnings(bool) {
  if (typeof bool !== 'boolean') {
    return new Error('Argument type: ' + (typeof bool === 'undefined' ? 'undefined' : _typeof(bool)) + '. Please use a boolean.');
  }
  deprecationWarnings_ = !bool;
  return 'adapter.js deprecation warnings ' + (bool ? 'disabled' : 'enabled');
}

function log() {
  if ((typeof window === 'undefined' ? 'undefined' : _typeof(window)) === 'object') {
    if (logDisabled_) {
      return;
    }
    if (typeof console !== 'undefined' && typeof console.log === 'function') {
      console.log.apply(console, arguments);
    }
  }
}

/**
 * Shows a deprecation warning suggesting the modern and spec-compatible API.
 */
function deprecated(oldMethod, newMethod) {
  if (!deprecationWarnings_) {
    return;
  }
  console.warn(oldMethod + ' is deprecated, please use ' + newMethod + ' instead.');
}

/**
 * Browser detector.
 *
 * @return {object} result containing browser and version
 *     properties.
 */
function detectBrowser(window) {
  var navigator = window.navigator;

  // Returned result object.

  var result = { browser: null, version: null };

  // Fail early if it's not a browser
  if (typeof window === 'undefined' || !window.navigator) {
    result.browser = 'Not a browser.';
    return result;
  }

  if (navigator.mozGetUserMedia) {
    // Firefox.
    result.browser = 'firefox';
    result.version = extractVersion(navigator.userAgent, /Firefox\/(\d+)\./, 1);
  } else if (navigator.webkitGetUserMedia || window.isSecureContext === false && window.webkitRTCPeerConnection && !window.RTCIceGatherer) {
    // Chrome, Chromium, Webview, Opera.
    // Version matches Chrome/WebRTC version.
    // Chrome 74 removed webkitGetUserMedia on http as well so we need the
    // more complicated fallback to webkitRTCPeerConnection.
    result.browser = 'chrome';
    result.version = extractVersion(navigator.userAgent, /Chrom(e|ium)\/(\d+)\./, 2);
  } else if (navigator.mediaDevices && navigator.userAgent.match(/Edge\/(\d+).(\d+)$/)) {
    // Edge.
    result.browser = 'edge';
    result.version = extractVersion(navigator.userAgent, /Edge\/(\d+).(\d+)$/, 2);
  } else if (window.RTCPeerConnection && navigator.userAgent.match(/AppleWebKit\/(\d+)\./)) {
    // Safari.
    result.browser = 'safari';
    result.version = extractVersion(navigator.userAgent, /AppleWebKit\/(\d+)\./, 1);
    result.supportsUnifiedPlan = window.RTCRtpTransceiver && 'currentDirection' in window.RTCRtpTransceiver.prototype;
  } else {
    // Default fallthrough: not supported.
    result.browser = 'Not a supported browser.';
    return result;
  }

  return result;
}

/**
 * Checks if something is an object.
 *
 * @param {*} val The something you want to check.
 * @return true if val is an object, false otherwise.
 */
function isObject(val) {
  return Object.prototype.toString.call(val) === '[object Object]';
}

/**
 * Remove all empty objects and undefined values
 * from a nested object -- an enhanced and vanilla version
 * of Lodash's `compact`.
 */
function compactObject(data) {
  if (!isObject(data)) {
    return data;
  }

  return Object.keys(data).reduce(function (accumulator, key) {
    var isObj = isObject(data[key]);
    var value = isObj ? compactObject(data[key]) : data[key];
    var isEmptyObject = isObj && !Object.keys(value).length;
    if (value === undefined || isEmptyObject) {
      return accumulator;
    }
    return Object.assign(accumulator, _defineProperty({}, key, value));
  }, {});
}

/* iterates the stats graph recursively. */
function walkStats(stats, base, resultSet) {
  if (!base || resultSet.has(base.id)) {
    return;
  }
  resultSet.set(base.id, base);
  Object.keys(base).forEach(function (name) {
    if (name.endsWith('Id')) {
      walkStats(stats, stats.get(base[name]), resultSet);
    } else if (name.endsWith('Ids')) {
      base[name].forEach(function (id) {
        walkStats(stats, stats.get(id), resultSet);
      });
    }
  });
}

/* filter getStats for a sender/receiver track. */
function filterStats(result, track, outbound) {
  var streamStatsType = outbound ? 'outbound-rtp' : 'inbound-rtp';
  var filteredResult = new Map();
  if (track === null) {
    return filteredResult;
  }
  var trackStats = [];
  result.forEach(function (value) {
    if (value.type === 'track' && value.trackIdentifier === track.id) {
      trackStats.push(value);
    }
  });
  trackStats.forEach(function (trackStat) {
    result.forEach(function (stats) {
      if (stats.type === streamStatsType && stats.trackId === trackStat.id) {
        walkStats(result, stats, filteredResult);
      }
    });
  });
  return filteredResult;
}

},{}],16:[function(require,module,exports){
/*
 *  Copyright (c) 2017 The WebRTC project authors. All Rights Reserved.
 *
 *  Use of this source code is governed by a BSD-style license
 *  that can be found in the LICENSE file in the root of the source
 *  tree.
 */
 /* eslint-env node */
'use strict';

var SDPUtils = require('sdp');

function fixStatsType(stat) {
  return {
    inboundrtp: 'inbound-rtp',
    outboundrtp: 'outbound-rtp',
    candidatepair: 'candidate-pair',
    localcandidate: 'local-candidate',
    remotecandidate: 'remote-candidate'
  }[stat.type] || stat.type;
}

function writeMediaSection(transceiver, caps, type, stream, dtlsRole) {
  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);

  // Map ICE parameters (ufrag, pwd) to SDP.
  sdp += SDPUtils.writeIceParameters(
      transceiver.iceGatherer.getLocalParameters());

  // Map DTLS parameters to SDP.
  sdp += SDPUtils.writeDtlsParameters(
      transceiver.dtlsTransport.getLocalParameters(),
      type === 'offer' ? 'actpass' : dtlsRole || 'active');

  sdp += 'a=mid:' + transceiver.mid + '\r\n';

  if (transceiver.rtpSender && transceiver.rtpReceiver) {
    sdp += 'a=sendrecv\r\n';
  } else if (transceiver.rtpSender) {
    sdp += 'a=sendonly\r\n';
  } else if (transceiver.rtpReceiver) {
    sdp += 'a=recvonly\r\n';
  } else {
    sdp += 'a=inactive\r\n';
  }

  if (transceiver.rtpSender) {
    var trackId = transceiver.rtpSender._initialTrackId ||
        transceiver.rtpSender.track.id;
    transceiver.rtpSender._initialTrackId = trackId;
    // spec.
    var msid = 'msid:' + (stream ? stream.id : '-') + ' ' +
        trackId + '\r\n';
    sdp += 'a=' + msid;
    // for Chrome. Legacy should no longer be required.
    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
        ' ' + msid;

    // RTX
    if (transceiver.sendEncodingParameters[0].rtx) {
      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
          ' ' + msid;
      sdp += 'a=ssrc-group:FID ' +
          transceiver.sendEncodingParameters[0].ssrc + ' ' +
          transceiver.sendEncodingParameters[0].rtx.ssrc +
          '\r\n';
    }
  }
  // FIXME: this should be written by writeRtpDescription.
  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
      ' cname:' + SDPUtils.localCName + '\r\n';
  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
        ' cname:' + SDPUtils.localCName + '\r\n';
  }
  return sdp;
}

// Edge does not like
// 1) stun: filtered after 14393 unless ?transport=udp is present
// 2) turn: that does not have all of turn:host:port?transport=udp
// 3) turn: with ipv6 addresses
// 4) turn: occurring muliple times
function filterIceServers(iceServers, edgeVersion) {
  var hasTurn = false;
  iceServers = JSON.parse(JSON.stringify(iceServers));
  return iceServers.filter(function(server) {
    if (server && (server.urls || server.url)) {
      var urls = server.urls || server.url;
      if (server.url && !server.urls) {
        console.warn('RTCIceServer.url is deprecated! Use urls instead.');
      }
      var isString = typeof urls === 'string';
      if (isString) {
        urls = [urls];
      }
      urls = urls.filter(function(url) {
        var validTurn = url.indexOf('turn:') === 0 &&
            url.indexOf('transport=udp') !== -1 &&
            url.indexOf('turn:[') === -1 &&
            !hasTurn;

        if (validTurn) {
          hasTurn = true;
          return true;
        }
        return url.indexOf('stun:') === 0 && edgeVersion >= 14393 &&
            url.indexOf('?transport=udp') === -1;
      });

      delete server.url;
      server.urls = isString ? urls[0] : urls;
      return !!urls.length;
    }
  });
}

// Determines the intersection of local and remote capabilities.
function getCommonCapabilities(localCapabilities, remoteCapabilities) {
  var commonCapabilities = {
    codecs: [],
    headerExtensions: [],
    fecMechanisms: []
  };

  var findCodecByPayloadType = function(pt, codecs) {
    pt = parseInt(pt, 10);
    for (var i = 0; i < codecs.length; i++) {
      if (codecs[i].payloadType === pt ||
          codecs[i].preferredPayloadType === pt) {
        return codecs[i];
      }
    }
  };

  var rtxCapabilityMatches = function(lRtx, rRtx, lCodecs, rCodecs) {
    var lCodec = findCodecByPayloadType(lRtx.parameters.apt, lCodecs);
    var rCodec = findCodecByPayloadType(rRtx.parameters.apt, rCodecs);
    return lCodec && rCodec &&
        lCodec.name.toLowerCase() === rCodec.name.toLowerCase();
  };

  localCapabilities.codecs.forEach(function(lCodec) {
    for (var i = 0; i < remoteCapabilities.codecs.length; i++) {
      var rCodec = remoteCapabilities.codecs[i];
      if (lCodec.name.toLowerCase() === rCodec.name.toLowerCase() &&
          lCodec.clockRate === rCodec.clockRate) {
        if (lCodec.name.toLowerCase() === 'rtx' &&
            lCodec.parameters && rCodec.parameters.apt) {
          // for RTX we need to find the local rtx that has a apt
          // which points to the same local codec as the remote one.
          if (!rtxCapabilityMatches(lCodec, rCodec,
              localCapabilities.codecs, remoteCapabilities.codecs)) {
            continue;
          }
        }
        rCodec = JSON.parse(JSON.stringify(rCodec)); // deepcopy
        // number of channels is the highest common number of channels
        rCodec.numChannels = Math.min(lCodec.numChannels,
            rCodec.numChannels);
        // push rCodec so we reply with offerer payload type
        commonCapabilities.codecs.push(rCodec);

        // determine common feedback mechanisms
        rCodec.rtcpFeedback = rCodec.rtcpFeedback.filter(function(fb) {
          for (var j = 0; j < lCodec.rtcpFeedback.length; j++) {
            if (lCodec.rtcpFeedback[j].type === fb.type &&
                lCodec.rtcpFeedback[j].parameter === fb.parameter) {
              return true;
            }
          }
          return false;
        });
        // FIXME: also need to determine .parameters
        //  see https://github.com/openpeer/ortc/issues/569
        break;
      }
    }
  });

  localCapabilities.headerExtensions.forEach(function(lHeaderExtension) {
    for (var i = 0; i < remoteCapabilities.headerExtensions.length;
         i++) {
      var rHeaderExtension = remoteCapabilities.headerExtensions[i];
      if (lHeaderExtension.uri === rHeaderExtension.uri) {
        commonCapabilities.headerExtensions.push(rHeaderExtension);
        break;
      }
    }
  });

  // FIXME: fecMechanisms
  return commonCapabilities;
}

// is action=setLocalDescription with type allowed in signalingState
function isActionAllowedInSignalingState(action, type, signalingState) {
  return {
    offer: {
      setLocalDescription: ['stable', 'have-local-offer'],
      setRemoteDescription: ['stable', 'have-remote-offer']
    },
    answer: {
      setLocalDescription: ['have-remote-offer', 'have-local-pranswer'],
      setRemoteDescription: ['have-local-offer', 'have-remote-pranswer']
    }
  }[type][action].indexOf(signalingState) !== -1;
}

function maybeAddCandidate(iceTransport, candidate) {
  // Edge's internal representation adds some fields therefore
  // not all fieldѕ are taken into account.
  var alreadyAdded = iceTransport.getRemoteCandidates()
      .find(function(remoteCandidate) {
        return candidate.foundation === remoteCandidate.foundation &&
            candidate.ip === remoteCandidate.ip &&
            candidate.port === remoteCandidate.port &&
            candidate.priority === remoteCandidate.priority &&
            candidate.protocol === remoteCandidate.protocol &&
            candidate.type === remoteCandidate.type;
      });
  if (!alreadyAdded) {
    iceTransport.addRemoteCandidate(candidate);
  }
  return !alreadyAdded;
}


function makeError(name, description) {
  var e = new Error(description);
  e.name = name;
  // legacy error codes from https://heycam.github.io/webidl/#idl-DOMException-error-names
  e.code = {
    NotSupportedError: 9,
    InvalidStateError: 11,
    InvalidAccessError: 15,
    TypeError: undefined,
    OperationError: undefined
  }[name];
  return e;
}

module.exports = function(window, edgeVersion) {
  // https://w3c.github.io/mediacapture-main/#mediastream
  // Helper function to add the track to the stream and
  // dispatch the event ourselves.
  function addTrackToStreamAndFireEvent(track, stream) {
    stream.addTrack(track);
    stream.dispatchEvent(new window.MediaStreamTrackEvent('addtrack',
        {track: track}));
  }

  function removeTrackFromStreamAndFireEvent(track, stream) {
    stream.removeTrack(track);
    stream.dispatchEvent(new window.MediaStreamTrackEvent('removetrack',
        {track: track}));
  }

  function fireAddTrack(pc, track, receiver, streams) {
    var trackEvent = new Event('track');
    trackEvent.track = track;
    trackEvent.receiver = receiver;
    trackEvent.transceiver = {receiver: receiver};
    trackEvent.streams = streams;
    window.setTimeout(function() {
      pc._dispatchEvent('track', trackEvent);
    });
  }

  var RTCPeerConnection = function(config) {
    var pc = this;

    var _eventTarget = document.createDocumentFragment();
    ['addEventListener', 'removeEventListener', 'dispatchEvent']
        .forEach(function(method) {
          pc[method] = _eventTarget[method].bind(_eventTarget);
        });

    this.canTrickleIceCandidates = null;

    this.needNegotiation = false;

    this.localStreams = [];
    this.remoteStreams = [];

    this._localDescription = null;
    this._remoteDescription = null;

    this.signalingState = 'stable';
    this.iceConnectionState = 'new';
    this.connectionState = 'new';
    this.iceGatheringState = 'new';

    config = JSON.parse(JSON.stringify(config || {}));

    this.usingBundle = config.bundlePolicy === 'max-bundle';
    if (config.rtcpMuxPolicy === 'negotiate') {
      throw(makeError('NotSupportedError',
          'rtcpMuxPolicy \'negotiate\' is not supported'));
    } else if (!config.rtcpMuxPolicy) {
      config.rtcpMuxPolicy = 'require';
    }

    switch (config.iceTransportPolicy) {
      case 'all':
      case 'relay':
        break;
      default:
        config.iceTransportPolicy = 'all';
        break;
    }

    switch (config.bundlePolicy) {
      case 'balanced':
      case 'max-compat':
      case 'max-bundle':
        break;
      default:
        config.bundlePolicy = 'balanced';
        break;
    }

    config.iceServers = filterIceServers(config.iceServers || [], edgeVersion);

    this._iceGatherers = [];
    if (config.iceCandidatePoolSize) {
      for (var i = config.iceCandidatePoolSize; i > 0; i--) {
        this._iceGatherers.push(new window.RTCIceGatherer({
          iceServers: config.iceServers,
          gatherPolicy: config.iceTransportPolicy
        }));
      }
    } else {
      config.iceCandidatePoolSize = 0;
    }

    this._config = config;

    // per-track iceGathers, iceTransports, dtlsTransports, rtpSenders, ...
    // everything that is needed to describe a SDP m-line.
    this.transceivers = [];

    this._sdpSessionId = SDPUtils.generateSessionId();
    this._sdpSessionVersion = 0;

    this._dtlsRole = undefined; // role for a=setup to use in answers.

    this._isClosed = false;
  };

  Object.defineProperty(RTCPeerConnection.prototype, 'localDescription', {
    configurable: true,
    get: function() {
      return this._localDescription;
    }
  });
  Object.defineProperty(RTCPeerConnection.prototype, 'remoteDescription', {
    configurable: true,
    get: function() {
      return this._remoteDescription;
    }
  });

  // set up event handlers on prototype
  RTCPeerConnection.prototype.onicecandidate = null;
  RTCPeerConnection.prototype.onaddstream = null;
  RTCPeerConnection.prototype.ontrack = null;
  RTCPeerConnection.prototype.onremovestream = null;
  RTCPeerConnection.prototype.onsignalingstatechange = null;
  RTCPeerConnection.prototype.oniceconnectionstatechange = null;
  RTCPeerConnection.prototype.onconnectionstatechange = null;
  RTCPeerConnection.prototype.onicegatheringstatechange = null;
  RTCPeerConnection.prototype.onnegotiationneeded = null;
  RTCPeerConnection.prototype.ondatachannel = null;

  RTCPeerConnection.prototype._dispatchEvent = function(name, event) {
    if (this._isClosed) {
      return;
    }
    this.dispatchEvent(event);
    if (typeof this['on' + name] === 'function') {
      this['on' + name](event);
    }
  };

  RTCPeerConnection.prototype._emitGatheringStateChange = function() {
    var event = new Event('icegatheringstatechange');
    this._dispatchEvent('icegatheringstatechange', event);
  };

  RTCPeerConnection.prototype.getConfiguration = function() {
    return this._config;
  };

  RTCPeerConnection.prototype.getLocalStreams = function() {
    return this.localStreams;
  };

  RTCPeerConnection.prototype.getRemoteStreams = function() {
    return this.remoteStreams;
  };

  // internal helper to create a transceiver object.
  // (which is not yet the same as the WebRTC 1.0 transceiver)
  RTCPeerConnection.prototype._createTransceiver = function(kind, doNotAdd) {
    var hasBundleTransport = this.transceivers.length > 0;
    var transceiver = {
      track: null,
      iceGatherer: null,
      iceTransport: null,
      dtlsTransport: null,
      localCapabilities: null,
      remoteCapabilities: null,
      rtpSender: null,
      rtpReceiver: null,
      kind: kind,
      mid: null,
      sendEncodingParameters: null,
      recvEncodingParameters: null,
      stream: null,
      associatedRemoteMediaStreams: [],
      wantReceive: true
    };
    if (this.usingBundle && hasBundleTransport) {
      transceiver.iceTransport = this.transceivers[0].iceTransport;
      transceiver.dtlsTransport = this.transceivers[0].dtlsTransport;
    } else {
      var transports = this._createIceAndDtlsTransports();
      transceiver.iceTransport = transports.iceTransport;
      transceiver.dtlsTransport = transports.dtlsTransport;
    }
    if (!doNotAdd) {
      this.transceivers.push(transceiver);
    }
    return transceiver;
  };

  RTCPeerConnection.prototype.addTrack = function(track, stream) {
    if (this._isClosed) {
      throw makeError('InvalidStateError',
          'Attempted to call addTrack on a closed peerconnection.');
    }

    var alreadyExists = this.transceivers.find(function(s) {
      return s.track === track;
    });

    if (alreadyExists) {
      throw makeError('InvalidAccessError', 'Track already exists.');
    }

    var transceiver;
    for (var i = 0; i < this.transceivers.length; i++) {
      if (!this.transceivers[i].track &&
          this.transceivers[i].kind === track.kind) {
        transceiver = this.transceivers[i];
      }
    }
    if (!transceiver) {
      transceiver = this._createTransceiver(track.kind);
    }

    this._maybeFireNegotiationNeeded();

    if (this.localStreams.indexOf(stream) === -1) {
      this.localStreams.push(stream);
    }

    transceiver.track = track;
    transceiver.stream = stream;
    transceiver.rtpSender = new window.RTCRtpSender(track,
        transceiver.dtlsTransport);
    return transceiver.rtpSender;
  };

  RTCPeerConnection.prototype.addStream = function(stream) {
    var pc = this;
    if (edgeVersion >= 15025) {
      stream.getTracks().forEach(function(track) {
        pc.addTrack(track, stream);
      });
    } else {
      // Clone is necessary for local demos mostly, attaching directly
      // to two different senders does not work (build 10547).
      // Fixed in 15025 (or earlier)
      var clonedStream = stream.clone();
      stream.getTracks().forEach(function(track, idx) {
        var clonedTrack = clonedStream.getTracks()[idx];
        track.addEventListener('enabled', function(event) {
          clonedTrack.enabled = event.enabled;
        });
      });
      clonedStream.getTracks().forEach(function(track) {
        pc.addTrack(track, clonedStream);
      });
    }
  };

  RTCPeerConnection.prototype.removeTrack = function(sender) {
    if (this._isClosed) {
      throw makeError('InvalidStateError',
          'Attempted to call removeTrack on a closed peerconnection.');
    }

    if (!(sender instanceof window.RTCRtpSender)) {
      throw new TypeError('Argument 1 of RTCPeerConnection.removeTrack ' +
          'does not implement interface RTCRtpSender.');
    }

    var transceiver = this.transceivers.find(function(t) {
      return t.rtpSender === sender;
    });

    if (!transceiver) {
      throw makeError('InvalidAccessError',
          'Sender was not created by this connection.');
    }
    var stream = transceiver.stream;

    transceiver.rtpSender.stop();
    transceiver.rtpSender = null;
    transceiver.track = null;
    transceiver.stream = null;

    // remove the stream from the set of local streams
    var localStreams = this.transceivers.map(function(t) {
      return t.stream;
    });
    if (localStreams.indexOf(stream) === -1 &&
        this.localStreams.indexOf(stream) > -1) {
      this.localStreams.splice(this.localStreams.indexOf(stream), 1);
    }

    this._maybeFireNegotiationNeeded();
  };

  RTCPeerConnection.prototype.removeStream = function(stream) {
    var pc = this;
    stream.getTracks().forEach(function(track) {
      var sender = pc.getSenders().find(function(s) {
        return s.track === track;
      });
      if (sender) {
        pc.removeTrack(sender);
      }
    });
  };

  RTCPeerConnection.prototype.getSenders = function() {
    return this.transceivers.filter(function(transceiver) {
      return !!transceiver.rtpSender;
    })
    .map(function(transceiver) {
      return transceiver.rtpSender;
    });
  };

  RTCPeerConnection.prototype.getReceivers = function() {
    return this.transceivers.filter(function(transceiver) {
      return !!transceiver.rtpReceiver;
    })
    .map(function(transceiver) {
      return transceiver.rtpReceiver;
    });
  };


  RTCPeerConnection.prototype._createIceGatherer = function(sdpMLineIndex,
      usingBundle) {
    var pc = this;
    if (usingBundle && sdpMLineIndex > 0) {
      return this.transceivers[0].iceGatherer;
    } else if (this._iceGatherers.length) {
      return this._iceGatherers.shift();
    }
    var iceGatherer = new window.RTCIceGatherer({
      iceServers: this._config.iceServers,
      gatherPolicy: this._config.iceTransportPolicy
    });
    Object.defineProperty(iceGatherer, 'state',
        {value: 'new', writable: true}
    );

    this.transceivers[sdpMLineIndex].bufferedCandidateEvents = [];
    this.transceivers[sdpMLineIndex].bufferCandidates = function(event) {
      var end = !event.candidate || Object.keys(event.candidate).length === 0;
      // polyfill since RTCIceGatherer.state is not implemented in
      // Edge 10547 yet.
      iceGatherer.state = end ? 'completed' : 'gathering';
      if (pc.transceivers[sdpMLineIndex].bufferedCandidateEvents !== null) {
        pc.transceivers[sdpMLineIndex].bufferedCandidateEvents.push(event);
      }
    };
    iceGatherer.addEventListener('localcandidate',
      this.transceivers[sdpMLineIndex].bufferCandidates);
    return iceGatherer;
  };

  // start gathering from an RTCIceGatherer.
  RTCPeerConnection.prototype._gather = function(mid, sdpMLineIndex) {
    var pc = this;
    var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
    if (iceGatherer.onlocalcandidate) {
      return;
    }
    var bufferedCandidateEvents =
      this.transceivers[sdpMLineIndex].bufferedCandidateEvents;
    this.transceivers[sdpMLineIndex].bufferedCandidateEvents = null;
    iceGatherer.removeEventListener('localcandidate',
      this.transceivers[sdpMLineIndex].bufferCandidates);
    iceGatherer.onlocalcandidate = function(evt) {
      if (pc.usingBundle && sdpMLineIndex > 0) {
        // if we know that we use bundle we can drop candidates with
        // ѕdpMLineIndex > 0. If we don't do this then our state gets
        // confused since we dispose the extra ice gatherer.
        return;
      }
      var event = new Event('icecandidate');
      event.candidate = {sdpMid: mid, sdpMLineIndex: sdpMLineIndex};

      var cand = evt.candidate;
      // Edge emits an empty object for RTCIceCandidateComplete‥
      var end = !cand || Object.keys(cand).length === 0;
      if (end) {
        // polyfill since RTCIceGatherer.state is not implemented in
        // Edge 10547 yet.
        if (iceGatherer.state === 'new' || iceGatherer.state === 'gathering') {
          iceGatherer.state = 'completed';
        }
      } else {
        if (iceGatherer.state === 'new') {
          iceGatherer.state = 'gathering';
        }
        // RTCIceCandidate doesn't have a component, needs to be added
        cand.component = 1;
        // also the usernameFragment. TODO: update SDP to take both variants.
        cand.ufrag = iceGatherer.getLocalParameters().usernameFragment;

        var serializedCandidate = SDPUtils.writeCandidate(cand);
        event.candidate = Object.assign(event.candidate,
            SDPUtils.parseCandidate(serializedCandidate));

        event.candidate.candidate = serializedCandidate;
        event.candidate.toJSON = function() {
          return {
            candidate: event.candidate.candidate,
            sdpMid: event.candidate.sdpMid,
            sdpMLineIndex: event.candidate.sdpMLineIndex,
            usernameFragment: event.candidate.usernameFragment
          };
        };
      }

      // update local description.
      var sections = SDPUtils.getMediaSections(pc._localDescription.sdp);
      if (!end) {
        sections[event.candidate.sdpMLineIndex] +=
            'a=' + event.candidate.candidate + '\r\n';
      } else {
        sections[event.candidate.sdpMLineIndex] +=
            'a=end-of-candidates\r\n';
      }
      pc._localDescription.sdp =
          SDPUtils.getDescription(pc._localDescription.sdp) +
          sections.join('');
      var complete = pc.transceivers.every(function(transceiver) {
        return transceiver.iceGatherer &&
            transceiver.iceGatherer.state === 'completed';
      });

      if (pc.iceGatheringState !== 'gathering') {
        pc.iceGatheringState = 'gathering';
        pc._emitGatheringStateChange();
      }

      // Emit candidate. Also emit null candidate when all gatherers are
      // complete.
      if (!end) {
        pc._dispatchEvent('icecandidate', event);
      }
      if (complete) {
        pc._dispatchEvent('icecandidate', new Event('icecandidate'));
        pc.iceGatheringState = 'complete';
        pc._emitGatheringStateChange();
      }
    };

    // emit already gathered candidates.
    window.setTimeout(function() {
      bufferedCandidateEvents.forEach(function(e) {
        iceGatherer.onlocalcandidate(e);
      });
    }, 0);
  };

  // Create ICE transport and DTLS transport.
  RTCPeerConnection.prototype._createIceAndDtlsTransports = function() {
    var pc = this;
    var iceTransport = new window.RTCIceTransport(null);
    iceTransport.onicestatechange = function() {
      pc._updateIceConnectionState();
      pc._updateConnectionState();
    };

    var dtlsTransport = new window.RTCDtlsTransport(iceTransport);
    dtlsTransport.ondtlsstatechange = function() {
      pc._updateConnectionState();
    };
    dtlsTransport.onerror = function() {
      // onerror does not set state to failed by itself.
      Object.defineProperty(dtlsTransport, 'state',
          {value: 'failed', writable: true});
      pc._updateConnectionState();
    };

    return {
      iceTransport: iceTransport,
      dtlsTransport: dtlsTransport
    };
  };

  // Destroy ICE gatherer, ICE transport and DTLS transport.
  // Without triggering the callbacks.
  RTCPeerConnection.prototype._disposeIceAndDtlsTransports = function(
      sdpMLineIndex) {
    var iceGatherer = this.transceivers[sdpMLineIndex].iceGatherer;
    if (iceGatherer) {
      delete iceGatherer.onlocalcandidate;
      delete this.transceivers[sdpMLineIndex].iceGatherer;
    }
    var iceTransport = this.transceivers[sdpMLineIndex].iceTransport;
    if (iceTransport) {
      delete iceTransport.onicestatechange;
      delete this.transceivers[sdpMLineIndex].iceTransport;
    }
    var dtlsTransport = this.transceivers[sdpMLineIndex].dtlsTransport;
    if (dtlsTransport) {
      delete dtlsTransport.ondtlsstatechange;
      delete dtlsTransport.onerror;
      delete this.transceivers[sdpMLineIndex].dtlsTransport;
    }
  };

  // Start the RTP Sender and Receiver for a transceiver.
  RTCPeerConnection.prototype._transceive = function(transceiver,
      send, recv) {
    var params = getCommonCapabilities(transceiver.localCapabilities,
        transceiver.remoteCapabilities);
    if (send && transceiver.rtpSender) {
      params.encodings = transceiver.sendEncodingParameters;
      params.rtcp = {
        cname: SDPUtils.localCName,
        compound: transceiver.rtcpParameters.compound
      };
      if (transceiver.recvEncodingParameters.length) {
        params.rtcp.ssrc = transceiver.recvEncodingParameters[0].ssrc;
      }
      transceiver.rtpSender.send(params);
    }
    if (recv && transceiver.rtpReceiver && params.codecs.length > 0) {
      // remove RTX field in Edge 14942
      if (transceiver.kind === 'video'
          && transceiver.recvEncodingParameters
          && edgeVersion < 15019) {
        transceiver.recvEncodingParameters.forEach(function(p) {
          delete p.rtx;
        });
      }
      if (transceiver.recvEncodingParameters.length) {
        params.encodings = transceiver.recvEncodingParameters;
      } else {
        params.encodings = [{}];
      }
      params.rtcp = {
        compound: transceiver.rtcpParameters.compound
      };
      if (transceiver.rtcpParameters.cname) {
        params.rtcp.cname = transceiver.rtcpParameters.cname;
      }
      if (transceiver.sendEncodingParameters.length) {
        params.rtcp.ssrc = transceiver.sendEncodingParameters[0].ssrc;
      }
      transceiver.rtpReceiver.receive(params);
    }
  };

  RTCPeerConnection.prototype.setLocalDescription = function(description) {
    var pc = this;

    // Note: pranswer is not supported.
    if (['offer', 'answer'].indexOf(description.type) === -1) {
      return Promise.reject(makeError('TypeError',
          'Unsupported type "' + description.type + '"'));
    }

    if (!isActionAllowedInSignalingState('setLocalDescription',
        description.type, pc.signalingState) || pc._isClosed) {
      return Promise.reject(makeError('InvalidStateError',
          'Can not set local ' + description.type +
          ' in state ' + pc.signalingState));
    }

    var sections;
    var sessionpart;
    if (description.type === 'offer') {
      // VERY limited support for SDP munging. Limited to:
      // * changing the order of codecs
      sections = SDPUtils.splitSections(description.sdp);
      sessionpart = sections.shift();
      sections.forEach(function(mediaSection, sdpMLineIndex) {
        var caps = SDPUtils.parseRtpParameters(mediaSection);
        pc.transceivers[sdpMLineIndex].localCapabilities = caps;
      });

      pc.transceivers.forEach(function(transceiver, sdpMLineIndex) {
        pc._gather(transceiver.mid, sdpMLineIndex);
      });
    } else if (description.type === 'answer') {
      sections = SDPUtils.splitSections(pc._remoteDescription.sdp);
      sessionpart = sections.shift();
      var isIceLite = SDPUtils.matchPrefix(sessionpart,
          'a=ice-lite').length > 0;
      sections.forEach(function(mediaSection, sdpMLineIndex) {
        var transceiver = pc.transceivers[sdpMLineIndex];
        var iceGatherer = transceiver.iceGatherer;
        var iceTransport = transceiver.iceTransport;
        var dtlsTransport = transceiver.dtlsTransport;
        var localCapabilities = transceiver.localCapabilities;
        var remoteCapabilities = transceiver.remoteCapabilities;

        // treat bundle-only as not-rejected.
        var rejected = SDPUtils.isRejected(mediaSection) &&
            SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 0;

        if (!rejected && !transceiver.rejected) {
          var remoteIceParameters = SDPUtils.getIceParameters(
              mediaSection, sessionpart);
          var remoteDtlsParameters = SDPUtils.getDtlsParameters(
              mediaSection, sessionpart);
          if (isIceLite) {
            remoteDtlsParameters.role = 'server';
          }

          if (!pc.usingBundle || sdpMLineIndex === 0) {
            pc._gather(transceiver.mid, sdpMLineIndex);
            if (iceTransport.state === 'new') {
              iceTransport.start(iceGatherer, remoteIceParameters,
                  isIceLite ? 'controlling' : 'controlled');
            }
            if (dtlsTransport.state === 'new') {
              dtlsTransport.start(remoteDtlsParameters);
            }
          }

          // Calculate intersection of capabilities.
          var params = getCommonCapabilities(localCapabilities,
              remoteCapabilities);

          // Start the RTCRtpSender. The RTCRtpReceiver for this
          // transceiver has already been started in setRemoteDescription.
          pc._transceive(transceiver,
              params.codecs.length > 0,
              false);
        }
      });
    }

    pc._localDescription = {
      type: description.type,
      sdp: description.sdp
    };
    if (description.type === 'offer') {
      pc._updateSignalingState('have-local-offer');
    } else {
      pc._updateSignalingState('stable');
    }

    return Promise.resolve();
  };

  RTCPeerConnection.prototype.setRemoteDescription = function(description) {
    var pc = this;

    // Note: pranswer is not supported.
    if (['offer', 'answer'].indexOf(description.type) === -1) {
      return Promise.reject(makeError('TypeError',
          'Unsupported type "' + description.type + '"'));
    }

    if (!isActionAllowedInSignalingState('setRemoteDescription',
        description.type, pc.signalingState) || pc._isClosed) {
      return Promise.reject(makeError('InvalidStateError',
          'Can not set remote ' + description.type +
          ' in state ' + pc.signalingState));
    }

    var streams = {};
    pc.remoteStreams.forEach(function(stream) {
      streams[stream.id] = stream;
    });
    var receiverList = [];
    var sections = SDPUtils.splitSections(description.sdp);
    var sessionpart = sections.shift();
    var isIceLite = SDPUtils.matchPrefix(sessionpart,
        'a=ice-lite').length > 0;
    var usingBundle = SDPUtils.matchPrefix(sessionpart,
        'a=group:BUNDLE ').length > 0;
    pc.usingBundle = usingBundle;
    var iceOptions = SDPUtils.matchPrefix(sessionpart,
        'a=ice-options:')[0];
    if (iceOptions) {
      pc.canTrickleIceCandidates = iceOptions.substr(14).split(' ')
          .indexOf('trickle') >= 0;
    } else {
      pc.canTrickleIceCandidates = false;
    }

    sections.forEach(function(mediaSection, sdpMLineIndex) {
      var lines = SDPUtils.splitLines(mediaSection);
      var kind = SDPUtils.getKind(mediaSection);
      // treat bundle-only as not-rejected.
      var rejected = SDPUtils.isRejected(mediaSection) &&
          SDPUtils.matchPrefix(mediaSection, 'a=bundle-only').length === 0;
      var protocol = lines[0].substr(2).split(' ')[2];

      var direction = SDPUtils.getDirection(mediaSection, sessionpart);
      var remoteMsid = SDPUtils.parseMsid(mediaSection);

      var mid = SDPUtils.getMid(mediaSection) || SDPUtils.generateIdentifier();

      // Reject datachannels which are not implemented yet.
      if (rejected || (kind === 'application' && (protocol === 'DTLS/SCTP' ||
          protocol === 'UDP/DTLS/SCTP'))) {
        // TODO: this is dangerous in the case where a non-rejected m-line
        //     becomes rejected.
        pc.transceivers[sdpMLineIndex] = {
          mid: mid,
          kind: kind,
          protocol: protocol,
          rejected: true
        };
        return;
      }

      if (!rejected && pc.transceivers[sdpMLineIndex] &&
          pc.transceivers[sdpMLineIndex].rejected) {
        // recycle a rejected transceiver.
        pc.transceivers[sdpMLineIndex] = pc._createTransceiver(kind, true);
      }

      var transceiver;
      var iceGatherer;
      var iceTransport;
      var dtlsTransport;
      var rtpReceiver;
      var sendEncodingParameters;
      var recvEncodingParameters;
      var localCapabilities;

      var track;
      // FIXME: ensure the mediaSection has rtcp-mux set.
      var remoteCapabilities = SDPUtils.parseRtpParameters(mediaSection);
      var remoteIceParameters;
      var remoteDtlsParameters;
      if (!rejected) {
        remoteIceParameters = SDPUtils.getIceParameters(mediaSection,
            sessionpart);
        remoteDtlsParameters = SDPUtils.getDtlsParameters(mediaSection,
            sessionpart);
        remoteDtlsParameters.role = 'client';
      }
      recvEncodingParameters =
          SDPUtils.parseRtpEncodingParameters(mediaSection);

      var rtcpParameters = SDPUtils.parseRtcpParameters(mediaSection);

      var isComplete = SDPUtils.matchPrefix(mediaSection,
          'a=end-of-candidates', sessionpart).length > 0;
      var cands = SDPUtils.matchPrefix(mediaSection, 'a=candidate:')
          .map(function(cand) {
            return SDPUtils.parseCandidate(cand);
          })
          .filter(function(cand) {
            return cand.component === 1;
          });

      // Check if we can use BUNDLE and dispose transports.
      if ((description.type === 'offer' || description.type === 'answer') &&
          !rejected && usingBundle && sdpMLineIndex > 0 &&
          pc.transceivers[sdpMLineIndex]) {
        pc._disposeIceAndDtlsTransports(sdpMLineIndex);
        pc.transceivers[sdpMLineIndex].iceGatherer =
            pc.transceivers[0].iceGatherer;
        pc.transceivers[sdpMLineIndex].iceTransport =
            pc.transceivers[0].iceTransport;
        pc.transceivers[sdpMLineIndex].dtlsTransport =
            pc.transceivers[0].dtlsTransport;
        if (pc.transceivers[sdpMLineIndex].rtpSender) {
          pc.transceivers[sdpMLineIndex].rtpSender.setTransport(
              pc.transceivers[0].dtlsTransport);
        }
        if (pc.transceivers[sdpMLineIndex].rtpReceiver) {
          pc.transceivers[sdpMLineIndex].rtpReceiver.setTransport(
              pc.transceivers[0].dtlsTransport);
        }
      }
      if (description.type === 'offer' && !rejected) {
        transceiver = pc.transceivers[sdpMLineIndex] ||
            pc._createTransceiver(kind);
        transceiver.mid = mid;

        if (!transceiver.iceGatherer) {
          transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex,
              usingBundle);
        }

        if (cands.length && transceiver.iceTransport.state === 'new') {
          if (isComplete && (!usingBundle || sdpMLineIndex === 0)) {
            transceiver.iceTransport.setRemoteCandidates(cands);
          } else {
            cands.forEach(function(candidate) {
              maybeAddCandidate(transceiver.iceTransport, candidate);
            });
          }
        }

        localCapabilities = window.RTCRtpReceiver.getCapabilities(kind);

        // filter RTX until additional stuff needed for RTX is implemented
        // in adapter.js
        if (edgeVersion < 15019) {
          localCapabilities.codecs = localCapabilities.codecs.filter(
              function(codec) {
                return codec.name !== 'rtx';
              });
        }

        sendEncodingParameters = transceiver.sendEncodingParameters || [{
          ssrc: (2 * sdpMLineIndex + 2) * 1001
        }];

        // TODO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams
        var isNewTrack = false;
        if (direction === 'sendrecv' || direction === 'sendonly') {
          isNewTrack = !transceiver.rtpReceiver;
          rtpReceiver = transceiver.rtpReceiver ||
              new window.RTCRtpReceiver(transceiver.dtlsTransport, kind);

          if (isNewTrack) {
            var stream;
            track = rtpReceiver.track;
            // FIXME: does not work with Plan B.
            if (remoteMsid && remoteMsid.stream === '-') {
              // no-op. a stream id of '-' means: no associated stream.
            } else if (remoteMsid) {
              if (!streams[remoteMsid.stream]) {
                streams[remoteMsid.stream] = new window.MediaStream();
                Object.defineProperty(streams[remoteMsid.stream], 'id', {
                  get: function() {
                    return remoteMsid.stream;
                  }
                });
              }
              Object.defineProperty(track, 'id', {
                get: function() {
                  return remoteMsid.track;
                }
              });
              stream = streams[remoteMsid.stream];
            } else {
              if (!streams.default) {
                streams.default = new window.MediaStream();
              }
              stream = streams.default;
            }
            if (stream) {
              addTrackToStreamAndFireEvent(track, stream);
              transceiver.associatedRemoteMediaStreams.push(stream);
            }
            receiverList.push([track, rtpReceiver, stream]);
          }
        } else if (transceiver.rtpReceiver && transceiver.rtpReceiver.track) {
          transceiver.associatedRemoteMediaStreams.forEach(function(s) {
            var nativeTrack = s.getTracks().find(function(t) {
              return t.id === transceiver.rtpReceiver.track.id;
            });
            if (nativeTrack) {
              removeTrackFromStreamAndFireEvent(nativeTrack, s);
            }
          });
          transceiver.associatedRemoteMediaStreams = [];
        }

        transceiver.localCapabilities = localCapabilities;
        transceiver.remoteCapabilities = remoteCapabilities;
        transceiver.rtpReceiver = rtpReceiver;
        transceiver.rtcpParameters = rtcpParameters;
        transceiver.sendEncodingParameters = sendEncodingParameters;
        transceiver.recvEncodingParameters = recvEncodingParameters;

        // Start the RTCRtpReceiver now. The RTPSender is started in
        // setLocalDescription.
        pc._transceive(pc.transceivers[sdpMLineIndex],
            false,
            isNewTrack);
      } else if (description.type === 'answer' && !rejected) {
        transceiver = pc.transceivers[sdpMLineIndex];
        iceGatherer = transceiver.iceGatherer;
        iceTransport = transceiver.iceTransport;
        dtlsTransport = transceiver.dtlsTransport;
        rtpReceiver = transceiver.rtpReceiver;
        sendEncodingParameters = transceiver.sendEncodingParameters;
        localCapabilities = transceiver.localCapabilities;

        pc.transceivers[sdpMLineIndex].recvEncodingParameters =
            recvEncodingParameters;
        pc.transceivers[sdpMLineIndex].remoteCapabilities =
            remoteCapabilities;
        pc.transceivers[sdpMLineIndex].rtcpParameters = rtcpParameters;

        if (cands.length && iceTransport.state === 'new') {
          if ((isIceLite || isComplete) &&
              (!usingBundle || sdpMLineIndex === 0)) {
            iceTransport.setRemoteCandidates(cands);
          } else {
            cands.forEach(function(candidate) {
              maybeAddCandidate(transceiver.iceTransport, candidate);
            });
          }
        }

        if (!usingBundle || sdpMLineIndex === 0) {
          if (iceTransport.state === 'new') {
            iceTransport.start(iceGatherer, remoteIceParameters,
                'controlling');
          }
          if (dtlsTransport.state === 'new') {
            dtlsTransport.start(remoteDtlsParameters);
          }
        }

        // If the offer contained RTX but the answer did not,
        // remove RTX from sendEncodingParameters.
        var commonCapabilities = getCommonCapabilities(
          transceiver.localCapabilities,
          transceiver.remoteCapabilities);

        var hasRtx = commonCapabilities.codecs.filter(function(c) {
          return c.name.toLowerCase() === 'rtx';
        }).length;
        if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) {
          delete transceiver.sendEncodingParameters[0].rtx;
        }

        pc._transceive(transceiver,
            direction === 'sendrecv' || direction === 'recvonly',
            direction === 'sendrecv' || direction === 'sendonly');

        // TODO: rewrite to use http://w3c.github.io/webrtc-pc/#set-associated-remote-streams
        if (rtpReceiver &&
            (direction === 'sendrecv' || direction === 'sendonly')) {
          track = rtpReceiver.track;
          if (remoteMsid) {
            if (!streams[remoteMsid.stream]) {
              streams[remoteMsid.stream] = new window.MediaStream();
            }
            addTrackToStreamAndFireEvent(track, streams[remoteMsid.stream]);
            receiverList.push([track, rtpReceiver, streams[remoteMsid.stream]]);
          } else {
            if (!streams.default) {
              streams.default = new window.MediaStream();
            }
            addTrackToStreamAndFireEvent(track, streams.default);
            receiverList.push([track, rtpReceiver, streams.default]);
          }
        } else {
          // FIXME: actually the receiver should be created later.
          delete transceiver.rtpReceiver;
        }
      }
    });

    if (pc._dtlsRole === undefined) {
      pc._dtlsRole = description.type === 'offer' ? 'active' : 'passive';
    }

    pc._remoteDescription = {
      type: description.type,
      sdp: description.sdp
    };
    if (description.type === 'offer') {
      pc._updateSignalingState('have-remote-offer');
    } else {
      pc._updateSignalingState('stable');
    }
    Object.keys(streams).forEach(function(sid) {
      var stream = streams[sid];
      if (stream.getTracks().length) {
        if (pc.remoteStreams.indexOf(stream) === -1) {
          pc.remoteStreams.push(stream);
          var event = new Event('addstream');
          event.stream = stream;
          window.setTimeout(function() {
            pc._dispatchEvent('addstream', event);
          });
        }

        receiverList.forEach(function(item) {
          var track = item[0];
          var receiver = item[1];
          if (stream.id !== item[2].id) {
            return;
          }
          fireAddTrack(pc, track, receiver, [stream]);
        });
      }
    });
    receiverList.forEach(function(item) {
      if (item[2]) {
        return;
      }
      fireAddTrack(pc, item[0], item[1], []);
    });

    // check whether addIceCandidate({}) was called within four seconds after
    // setRemoteDescription.
    window.setTimeout(function() {
      if (!(pc && pc.transceivers)) {
        return;
      }
      pc.transceivers.forEach(function(transceiver) {
        if (transceiver.iceTransport &&
            transceiver.iceTransport.state === 'new' &&
            transceiver.iceTransport.getRemoteCandidates().length > 0) {
          console.warn('Timeout for addRemoteCandidate. Consider sending ' +
              'an end-of-candidates notification');
          transceiver.iceTransport.addRemoteCandidate({});
        }
      });
    }, 4000);

    return Promise.resolve();
  };

  RTCPeerConnection.prototype.close = function() {
    this.transceivers.forEach(function(transceiver) {
      /* not yet
      if (transceiver.iceGatherer) {
        transceiver.iceGatherer.close();
      }
      */
      if (transceiver.iceTransport) {
        transceiver.iceTransport.stop();
      }
      if (transceiver.dtlsTransport) {
        transceiver.dtlsTransport.stop();
      }
      if (transceiver.rtpSender) {
        transceiver.rtpSender.stop();
      }
      if (transceiver.rtpReceiver) {
        transceiver.rtpReceiver.stop();
      }
    });
    // FIXME: clean up tracks, local streams, remote streams, etc
    this._isClosed = true;
    this._updateSignalingState('closed');
  };

  // Update the signaling state.
  RTCPeerConnection.prototype._updateSignalingState = function(newState) {
    this.signalingState = newState;
    var event = new Event('signalingstatechange');
    this._dispatchEvent('signalingstatechange', event);
  };

  // Determine whether to fire the negotiationneeded event.
  RTCPeerConnection.prototype._maybeFireNegotiationNeeded = function() {
    var pc = this;
    if (this.signalingState !== 'stable' || this.needNegotiation === true) {
      return;
    }
    this.needNegotiation = true;
    window.setTimeout(function() {
      if (pc.needNegotiation) {
        pc.needNegotiation = false;
        var event = new Event('negotiationneeded');
        pc._dispatchEvent('negotiationneeded', event);
      }
    }, 0);
  };

  // Update the ice connection state.
  RTCPeerConnection.prototype._updateIceConnectionState = function() {
    var newState;
    var states = {
      'new': 0,
      closed: 0,
      checking: 0,
      connected: 0,
      completed: 0,
      disconnected: 0,
      failed: 0
    };
    this.transceivers.forEach(function(transceiver) {
      if (transceiver.iceTransport && !transceiver.rejected) {
        states[transceiver.iceTransport.state]++;
      }
    });

    newState = 'new';
    if (states.failed > 0) {
      newState = 'failed';
    } else if (states.checking > 0) {
      newState = 'checking';
    } else if (states.disconnected > 0) {
      newState = 'disconnected';
    } else if (states.new > 0) {
      newState = 'new';
    } else if (states.connected > 0) {
      newState = 'connected';
    } else if (states.completed > 0) {
      newState = 'completed';
    }

    if (newState !== this.iceConnectionState) {
      this.iceConnectionState = newState;
      var event = new Event('iceconnectionstatechange');
      this._dispatchEvent('iceconnectionstatechange', event);
    }
  };

  // Update the connection state.
  RTCPeerConnection.prototype._updateConnectionState = function() {
    var newState;
    var states = {
      'new': 0,
      closed: 0,
      connecting: 0,
      connected: 0,
      completed: 0,
      disconnected: 0,
      failed: 0
    };
    this.transceivers.forEach(function(transceiver) {
      if (transceiver.iceTransport && transceiver.dtlsTransport &&
          !transceiver.rejected) {
        states[transceiver.iceTransport.state]++;
        states[transceiver.dtlsTransport.state]++;
      }
    });
    // ICETransport.completed and connected are the same for this purpose.
    states.connected += states.completed;

    newState = 'new';
    if (states.failed > 0) {
      newState = 'failed';
    } else if (states.connecting > 0) {
      newState = 'connecting';
    } else if (states.disconnected > 0) {
      newState = 'disconnected';
    } else if (states.new > 0) {
      newState = 'new';
    } else if (states.connected > 0) {
      newState = 'connected';
    }

    if (newState !== this.connectionState) {
      this.connectionState = newState;
      var event = new Event('connectionstatechange');
      this._dispatchEvent('connectionstatechange', event);
    }
  };

  RTCPeerConnection.prototype.createOffer = function() {
    var pc = this;

    if (pc._isClosed) {
      return Promise.reject(makeError('InvalidStateError',
          'Can not call createOffer after close'));
    }

    var numAudioTracks = pc.transceivers.filter(function(t) {
      return t.kind === 'audio';
    }).length;
    var numVideoTracks = pc.transceivers.filter(function(t) {
      return t.kind === 'video';
    }).length;

    // Determine number of audio and video tracks we need to send/recv.
    var offerOptions = arguments[0];
    if (offerOptions) {
      // Reject Chrome legacy constraints.
      if (offerOptions.mandatory || offerOptions.optional) {
        throw new TypeError(
            'Legacy mandatory/optional constraints not supported.');
      }
      if (offerOptions.offerToReceiveAudio !== undefined) {
        if (offerOptions.offerToReceiveAudio === true) {
          numAudioTracks = 1;
        } else if (offerOptions.offerToReceiveAudio === false) {
          numAudioTracks = 0;
        } else {
          numAudioTracks = offerOptions.offerToReceiveAudio;
        }
      }
      if (offerOptions.offerToReceiveVideo !== undefined) {
        if (offerOptions.offerToReceiveVideo === true) {
          numVideoTracks = 1;
        } else if (offerOptions.offerToReceiveVideo === false) {
          numVideoTracks = 0;
        } else {
          numVideoTracks = offerOptions.offerToReceiveVideo;
        }
      }
    }

    pc.transceivers.forEach(function(transceiver) {
      if (transceiver.kind === 'audio') {
        numAudioTracks--;
        if (numAudioTracks < 0) {
          transceiver.wantReceive = false;
        }
      } else if (transceiver.kind === 'video') {
        numVideoTracks--;
        if (numVideoTracks < 0) {
          transceiver.wantReceive = false;
        }
      }
    });

    // Create M-lines for recvonly streams.
    while (numAudioTracks > 0 || numVideoTracks > 0) {
      if (numAudioTracks > 0) {
        pc._createTransceiver('audio');
        numAudioTracks--;
      }
      if (numVideoTracks > 0) {
        pc._createTransceiver('video');
        numVideoTracks--;
      }
    }

    var sdp = SDPUtils.writeSessionBoilerplate(pc._sdpSessionId,
        pc._sdpSessionVersion++);
    pc.transceivers.forEach(function(transceiver, sdpMLineIndex) {
      // For each track, create an ice gatherer, ice transport,
      // dtls transport, potentially rtpsender and rtpreceiver.
      var track = transceiver.track;
      var kind = transceiver.kind;
      var mid = transceiver.mid || SDPUtils.generateIdentifier();
      transceiver.mid = mid;

      if (!transceiver.iceGatherer) {
        transceiver.iceGatherer = pc._createIceGatherer(sdpMLineIndex,
            pc.usingBundle);
      }

      var localCapabilities = window.RTCRtpSender.getCapabilities(kind);
      // filter RTX until additional stuff needed for RTX is implemented
      // in adapter.js
      if (edgeVersion < 15019) {
        localCapabilities.codecs = localCapabilities.codecs.filter(
            function(codec) {
              return codec.name !== 'rtx';
            });
      }
      localCapabilities.codecs.forEach(function(codec) {
        // work around https://bugs.chromium.org/p/webrtc/issues/detail?id=6552
        // by adding level-asymmetry-allowed=1
        if (codec.name === 'H264' &&
            codec.parameters['level-asymmetry-allowed'] === undefined) {
          codec.parameters['level-asymmetry-allowed'] = '1';
        }

        // for subsequent offers, we might have to re-use the payload
        // type of the last offer.
        if (transceiver.remoteCapabilities &&
            transceiver.remoteCapabilities.codecs) {
          transceiver.remoteCapabilities.codecs.forEach(function(remoteCodec) {
            if (codec.name.toLowerCase() === remoteCodec.name.toLowerCase() &&
                codec.clockRate === remoteCodec.clockRate) {
              codec.preferredPayloadType = remoteCodec.payloadType;
            }
          });
        }
      });
      localCapabilities.headerExtensions.forEach(function(hdrExt) {
        var remoteExtensions = transceiver.remoteCapabilities &&
            transceiver.remoteCapabilities.headerExtensions || [];
        remoteExtensions.forEach(function(rHdrExt) {
          if (hdrExt.uri === rHdrExt.uri) {
            hdrExt.id = rHdrExt.id;
          }
        });
      });

      // generate an ssrc now, to be used later in rtpSender.send
      var sendEncodingParameters = transceiver.sendEncodingParameters || [{
        ssrc: (2 * sdpMLineIndex + 1) * 1001
      }];
      if (track) {
        // add RTX
        if (edgeVersion >= 15019 && kind === 'video' &&
            !sendEncodingParameters[0].rtx) {
          sendEncodingParameters[0].rtx = {
            ssrc: sendEncodingParameters[0].ssrc + 1
          };
        }
      }

      if (transceiver.wantReceive) {
        transceiver.rtpReceiver = new window.RTCRtpReceiver(
            transceiver.dtlsTransport, kind);
      }

      transceiver.localCapabilities = localCapabilities;
      transceiver.sendEncodingParameters = sendEncodingParameters;
    });

    // always offer BUNDLE and dispose on return if not supported.
    if (pc._config.bundlePolicy !== 'max-compat') {
      sdp += 'a=group:BUNDLE ' + pc.transceivers.map(function(t) {
        return t.mid;
      }).join(' ') + '\r\n';
    }
    sdp += 'a=ice-options:trickle\r\n';

    pc.transceivers.forEach(function(transceiver, sdpMLineIndex) {
      sdp += writeMediaSection(transceiver, transceiver.localCapabilities,
          'offer', transceiver.stream, pc._dtlsRole);
      sdp += 'a=rtcp-rsize\r\n';

      if (transceiver.iceGatherer && pc.iceGatheringState !== 'new' &&
          (sdpMLineIndex === 0 || !pc.usingBundle)) {
        transceiver.iceGatherer.getLocalCandidates().forEach(function(cand) {
          cand.component = 1;
          sdp += 'a=' + SDPUtils.writeCandidate(cand) + '\r\n';
        });

        if (transceiver.iceGatherer.state === 'completed') {
          sdp += 'a=end-of-candidates\r\n';
        }
      }
    });

    var desc = new window.RTCSessionDescription({
      type: 'offer',
      sdp: sdp
    });
    return Promise.resolve(desc);
  };

  RTCPeerConnection.prototype.createAnswer = function() {
    var pc = this;

    if (pc._isClosed) {
      return Promise.reject(makeError('InvalidStateError',
          'Can not call createAnswer after close'));
    }

    if (!(pc.signalingState === 'have-remote-offer' ||
        pc.signalingState === 'have-local-pranswer')) {
      return Promise.reject(makeError('InvalidStateError',
          'Can not call createAnswer in signalingState ' + pc.signalingState));
    }

    var sdp = SDPUtils.writeSessionBoilerplate(pc._sdpSessionId,
        pc._sdpSessionVersion++);
    if (pc.usingBundle) {
      sdp += 'a=group:BUNDLE ' + pc.transceivers.map(function(t) {
        return t.mid;
      }).join(' ') + '\r\n';
    }
    sdp += 'a=ice-options:trickle\r\n';

    var mediaSectionsInOffer = SDPUtils.getMediaSections(
        pc._remoteDescription.sdp).length;
    pc.transceivers.forEach(function(transceiver, sdpMLineIndex) {
      if (sdpMLineIndex + 1 > mediaSectionsInOffer) {
        return;
      }
      if (transceiver.rejected) {
        if (transceiver.kind === 'application') {
          if (transceiver.protocol === 'DTLS/SCTP') { // legacy fmt
            sdp += 'm=application 0 DTLS/SCTP 5000\r\n';
          } else {
            sdp += 'm=application 0 ' + transceiver.protocol +
                ' webrtc-datachannel\r\n';
          }
        } else if (transceiver.kind === 'audio') {
          sdp += 'm=audio 0 UDP/TLS/RTP/SAVPF 0\r\n' +
              'a=rtpmap:0 PCMU/8000\r\n';
        } else if (transceiver.kind === 'video') {
          sdp += 'm=video 0 UDP/TLS/RTP/SAVPF 120\r\n' +
              'a=rtpmap:120 VP8/90000\r\n';
        }
        sdp += 'c=IN IP4 0.0.0.0\r\n' +
            'a=inactive\r\n' +
            'a=mid:' + transceiver.mid + '\r\n';
        return;
      }

      // FIXME: look at direction.
      if (transceiver.stream) {
        var localTrack;
        if (transceiver.kind === 'audio') {
          localTrack = transceiver.stream.getAudioTracks()[0];
        } else if (transceiver.kind === 'video') {
          localTrack = transceiver.stream.getVideoTracks()[0];
        }
        if (localTrack) {
          // add RTX
          if (edgeVersion >= 15019 && transceiver.kind === 'video' &&
              !transceiver.sendEncodingParameters[0].rtx) {
            transceiver.sendEncodingParameters[0].rtx = {
              ssrc: transceiver.sendEncodingParameters[0].ssrc + 1
            };
          }
        }
      }

      // Calculate intersection of capabilities.
      var commonCapabilities = getCommonCapabilities(
          transceiver.localCapabilities,
          transceiver.remoteCapabilities);

      var hasRtx = commonCapabilities.codecs.filter(function(c) {
        return c.name.toLowerCase() === 'rtx';
      }).length;
      if (!hasRtx && transceiver.sendEncodingParameters[0].rtx) {
        delete transceiver.sendEncodingParameters[0].rtx;
      }

      sdp += writeMediaSection(transceiver, commonCapabilities,
          'answer', transceiver.stream, pc._dtlsRole);
      if (transceiver.rtcpParameters &&
          transceiver.rtcpParameters.reducedSize) {
        sdp += 'a=rtcp-rsize\r\n';
      }
    });

    var desc = new window.RTCSessionDescription({
      type: 'answer',
      sdp: sdp
    });
    return Promise.resolve(desc);
  };

  RTCPeerConnection.prototype.addIceCandidate = function(candidate) {
    var pc = this;
    var sections;
    if (candidate && !(candidate.sdpMLineIndex !== undefined ||
        candidate.sdpMid)) {
      return Promise.reject(new TypeError('sdpMLineIndex or sdpMid required'));
    }

    // TODO: needs to go into ops queue.
    return new Promise(function(resolve, reject) {
      if (!pc._remoteDescription) {
        return reject(makeError('InvalidStateError',
            'Can not add ICE candidate without a remote description'));
      } else if (!candidate || candidate.candidate === '') {
        for (var j = 0; j < pc.transceivers.length; j++) {
          if (pc.transceivers[j].rejected) {
            continue;
          }
          pc.transceivers[j].iceTransport.addRemoteCandidate({});
          sections = SDPUtils.getMediaSections(pc._remoteDescription.sdp);
          sections[j] += 'a=end-of-candidates\r\n';
          pc._remoteDescription.sdp =
              SDPUtils.getDescription(pc._remoteDescription.sdp) +
              sections.join('');
          if (pc.usingBundle) {
            break;
          }
        }
      } else {
        var sdpMLineIndex = candidate.sdpMLineIndex;
        if (candidate.sdpMid) {
          for (var i = 0; i < pc.transceivers.length; i++) {
            if (pc.transceivers[i].mid === candidate.sdpMid) {
              sdpMLineIndex = i;
              break;
            }
          }
        }
        var transceiver = pc.transceivers[sdpMLineIndex];
        if (transceiver) {
          if (transceiver.rejected) {
            return resolve();
          }
          var cand = Object.keys(candidate.candidate).length > 0 ?
              SDPUtils.parseCandidate(candidate.candidate) : {};
          // Ignore Chrome's invalid candidates since Edge does not like them.
          if (cand.protocol === 'tcp' && (cand.port === 0 || cand.port === 9)) {
            return resolve();
          }
          // Ignore RTCP candidates, we assume RTCP-MUX.
          if (cand.component && cand.component !== 1) {
            return resolve();
          }
          // when using bundle, avoid adding candidates to the wrong
          // ice transport. And avoid adding candidates added in the SDP.
          if (sdpMLineIndex === 0 || (sdpMLineIndex > 0 &&
              transceiver.iceTransport !== pc.transceivers[0].iceTransport)) {
            if (!maybeAddCandidate(transceiver.iceTransport, cand)) {
              return reject(makeError('OperationError',
                  'Can not add ICE candidate'));
            }
          }

          // update the remoteDescription.
          var candidateString = candidate.candidate.trim();
          if (candidateString.indexOf('a=') === 0) {
            candidateString = candidateString.substr(2);
          }
          sections = SDPUtils.getMediaSections(pc._remoteDescription.sdp);
          sections[sdpMLineIndex] += 'a=' +
              (cand.type ? candidateString : 'end-of-candidates')
              + '\r\n';
          pc._remoteDescription.sdp =
              SDPUtils.getDescription(pc._remoteDescription.sdp) +
              sections.join('');
        } else {
          return reject(makeError('OperationError',
              'Can not add ICE candidate'));
        }
      }
      resolve();
    });
  };

  RTCPeerConnection.prototype.getStats = function(selector) {
    if (selector && selector instanceof window.MediaStreamTrack) {
      var senderOrReceiver = null;
      this.transceivers.forEach(function(transceiver) {
        if (transceiver.rtpSender &&
            transceiver.rtpSender.track === selector) {
          senderOrReceiver = transceiver.rtpSender;
        } else if (transceiver.rtpReceiver &&
            transceiver.rtpReceiver.track === selector) {
          senderOrReceiver = transceiver.rtpReceiver;
        }
      });
      if (!senderOrReceiver) {
        throw makeError('InvalidAccessError', 'Invalid selector.');
      }
      return senderOrReceiver.getStats();
    }

    var promises = [];
    this.transceivers.forEach(function(transceiver) {
      ['rtpSender', 'rtpReceiver', 'iceGatherer', 'iceTransport',
          'dtlsTransport'].forEach(function(method) {
            if (transceiver[method]) {
              promises.push(transceiver[method].getStats());
            }
          });
    });
    return Promise.all(promises).then(function(allStats) {
      var results = new Map();
      allStats.forEach(function(stats) {
        stats.forEach(function(stat) {
          results.set(stat.id, stat);
        });
      });
      return results;
    });
  };

  // fix low-level stat names and return Map instead of object.
  var ortcObjects = ['RTCRtpSender', 'RTCRtpReceiver', 'RTCIceGatherer',
    'RTCIceTransport', 'RTCDtlsTransport'];
  ortcObjects.forEach(function(ortcObjectName) {
    var obj = window[ortcObjectName];
    if (obj && obj.prototype && obj.prototype.getStats) {
      var nativeGetstats = obj.prototype.getStats;
      obj.prototype.getStats = function() {
        return nativeGetstats.apply(this)
        .then(function(nativeStats) {
          var mapStats = new Map();
          Object.keys(nativeStats).forEach(function(id) {
            nativeStats[id].type = fixStatsType(nativeStats[id]);
            mapStats.set(id, nativeStats[id]);
          });
          return mapStats;
        });
      };
    }
  });

  // legacy callback shims. Should be moved to adapter.js some days.
  var methods = ['createOffer', 'createAnswer'];
  methods.forEach(function(method) {
    var nativeMethod = RTCPeerConnection.prototype[method];
    RTCPeerConnection.prototype[method] = function() {
      var args = arguments;
      if (typeof args[0] === 'function' ||
          typeof args[1] === 'function') { // legacy
        return nativeMethod.apply(this, [arguments[2]])
        .then(function(description) {
          if (typeof args[0] === 'function') {
            args[0].apply(null, [description]);
          }
        }, function(error) {
          if (typeof args[1] === 'function') {
            args[1].apply(null, [error]);
          }
        });
      }
      return nativeMethod.apply(this, arguments);
    };
  });

  methods = ['setLocalDescription', 'setRemoteDescription', 'addIceCandidate'];
  methods.forEach(function(method) {
    var nativeMethod = RTCPeerConnection.prototype[method];
    RTCPeerConnection.prototype[method] = function() {
      var args = arguments;
      if (typeof args[1] === 'function' ||
          typeof args[2] === 'function') { // legacy
        return nativeMethod.apply(this, arguments)
        .then(function() {
          if (typeof args[1] === 'function') {
            args[1].apply(null);
          }
        }, function(error) {
          if (typeof args[2] === 'function') {
            args[2].apply(null, [error]);
          }
        });
      }
      return nativeMethod.apply(this, arguments);
    };
  });

  // getStats is special. It doesn't have a spec legacy method yet we support
  // getStats(something, cb) without error callbacks.
  ['getStats'].forEach(function(method) {
    var nativeMethod = RTCPeerConnection.prototype[method];
    RTCPeerConnection.prototype[method] = function() {
      var args = arguments;
      if (typeof args[1] === 'function') {
        return nativeMethod.apply(this, arguments)
        .then(function() {
          if (typeof args[1] === 'function') {
            args[1].apply(null);
          }
        });
      }
      return nativeMethod.apply(this, arguments);
    };
  });

  return RTCPeerConnection;
};

},{"sdp":17}],17:[function(require,module,exports){
/* eslint-env node */
'use strict';

// SDP helpers.
var SDPUtils = {};

// Generate an alphanumeric identifier for cname or mids.
// TODO: use UUIDs instead? https://gist.github.com/jed/982883
SDPUtils.generateIdentifier = function() {
  return Math.random().toString(36).substr(2, 10);
};

// The RTCP CNAME used by all peerconnections from the same JS.
SDPUtils.localCName = SDPUtils.generateIdentifier();

// Splits SDP into lines, dealing with both CRLF and LF.
SDPUtils.splitLines = function(blob) {
  return blob.trim().split('\n').map(function(line) {
    return line.trim();
  });
};
// Splits SDP into sessionpart and mediasections. Ensures CRLF.
SDPUtils.splitSections = function(blob) {
  var parts = blob.split('\nm=');
  return parts.map(function(part, index) {
    return (index > 0 ? 'm=' + part : part).trim() + '\r\n';
  });
};

// returns the session description.
SDPUtils.getDescription = function(blob) {
  var sections = SDPUtils.splitSections(blob);
  return sections && sections[0];
};

// returns the individual media sections.
SDPUtils.getMediaSections = function(blob) {
  var sections = SDPUtils.splitSections(blob);
  sections.shift();
  return sections;
};

// Returns lines that start with a certain prefix.
SDPUtils.matchPrefix = function(blob, prefix) {
  return SDPUtils.splitLines(blob).filter(function(line) {
    return line.indexOf(prefix) === 0;
  });
};

// Parses an ICE candidate line. Sample input:
// candidate:702786350 2 udp 41819902 8.8.8.8 60769 typ relay raddr 8.8.8.8
// rport 55996"
SDPUtils.parseCandidate = function(line) {
  var parts;
  // Parse both variants.
  if (line.indexOf('a=candidate:') === 0) {
    parts = line.substring(12).split(' ');
  } else {
    parts = line.substring(10).split(' ');
  }

  var candidate = {
    foundation: parts[0],
    component: parseInt(parts[1], 10),
    protocol: parts[2].toLowerCase(),
    priority: parseInt(parts[3], 10),
    ip: parts[4],
    address: parts[4], // address is an alias for ip.
    port: parseInt(parts[5], 10),
    // skip parts[6] == 'typ'
    type: parts[7]
  };

  for (var i = 8; i < parts.length; i += 2) {
    switch (parts[i]) {
      case 'raddr':
        candidate.relatedAddress = parts[i + 1];
        break;
      case 'rport':
        candidate.relatedPort = parseInt(parts[i + 1], 10);
        break;
      case 'tcptype':
        candidate.tcpType = parts[i + 1];
        break;
      case 'ufrag':
        candidate.ufrag = parts[i + 1]; // for backward compability.
        candidate.usernameFragment = parts[i + 1];
        break;
      default: // extension handling, in particular ufrag
        candidate[parts[i]] = parts[i + 1];
        break;
    }
  }
  return candidate;
};

// Translates a candidate object into SDP candidate attribute.
SDPUtils.writeCandidate = function(candidate) {
  var sdp = [];
  sdp.push(candidate.foundation);
  sdp.push(candidate.component);
  sdp.push(candidate.protocol.toUpperCase());
  sdp.push(candidate.priority);
  sdp.push(candidate.address || candidate.ip);
  sdp.push(candidate.port);

  var type = candidate.type;
  sdp.push('typ');
  sdp.push(type);
  if (type !== 'host' && candidate.relatedAddress &&
      candidate.relatedPort) {
    sdp.push('raddr');
    sdp.push(candidate.relatedAddress);
    sdp.push('rport');
    sdp.push(candidate.relatedPort);
  }
  if (candidate.tcpType && candidate.protocol.toLowerCase() === 'tcp') {
    sdp.push('tcptype');
    sdp.push(candidate.tcpType);
  }
  if (candidate.usernameFragment || candidate.ufrag) {
    sdp.push('ufrag');
    sdp.push(candidate.usernameFragment || candidate.ufrag);
  }
  return 'candidate:' + sdp.join(' ');
};

// Parses an ice-options line, returns an array of option tags.
// a=ice-options:foo bar
SDPUtils.parseIceOptions = function(line) {
  return line.substr(14).split(' ');
};

// Parses an rtpmap line, returns RTCRtpCoddecParameters. Sample input:
// a=rtpmap:111 opus/48000/2
SDPUtils.parseRtpMap = function(line) {
  var parts = line.substr(9).split(' ');
  var parsed = {
    payloadType: parseInt(parts.shift(), 10) // was: id
  };

  parts = parts[0].split('/');

  parsed.name = parts[0];
  parsed.clockRate = parseInt(parts[1], 10); // was: clockrate
  parsed.channels = parts.length === 3 ? parseInt(parts[2], 10) : 1;
  // legacy alias, got renamed back to channels in ORTC.
  parsed.numChannels = parsed.channels;
  return parsed;
};

// Generate an a=rtpmap line from RTCRtpCodecCapability or
// RTCRtpCodecParameters.
SDPUtils.writeRtpMap = function(codec) {
  var pt = codec.payloadType;
  if (codec.preferredPayloadType !== undefined) {
    pt = codec.preferredPayloadType;
  }
  var channels = codec.channels || codec.numChannels || 1;
  return 'a=rtpmap:' + pt + ' ' + codec.name + '/' + codec.clockRate +
      (channels !== 1 ? '/' + channels : '') + '\r\n';
};

// Parses an a=extmap line (headerextension from RFC 5285). Sample input:
// a=extmap:2 urn:ietf:params:rtp-hdrext:toffset
// a=extmap:2/sendonly urn:ietf:params:rtp-hdrext:toffset
SDPUtils.parseExtmap = function(line) {
  var parts = line.substr(9).split(' ');
  return {
    id: parseInt(parts[0], 10),
    direction: parts[0].indexOf('/') > 0 ? parts[0].split('/')[1] : 'sendrecv',
    uri: parts[1]
  };
};

// Generates a=extmap line from RTCRtpHeaderExtensionParameters or
// RTCRtpHeaderExtension.
SDPUtils.writeExtmap = function(headerExtension) {
  return 'a=extmap:' + (headerExtension.id || headerExtension.preferredId) +
      (headerExtension.direction && headerExtension.direction !== 'sendrecv'
        ? '/' + headerExtension.direction
        : '') +
      ' ' + headerExtension.uri + '\r\n';
};

// Parses an ftmp line, returns dictionary. Sample input:
// a=fmtp:96 vbr=on;cng=on
// Also deals with vbr=on; cng=on
SDPUtils.parseFmtp = function(line) {
  var parsed = {};
  var kv;
  var parts = line.substr(line.indexOf(' ') + 1).split(';');
  for (var j = 0; j < parts.length; j++) {
    kv = parts[j].trim().split('=');
    parsed[kv[0].trim()] = kv[1];
  }
  return parsed;
};

// Generates an a=ftmp line from RTCRtpCodecCapability or RTCRtpCodecParameters.
SDPUtils.writeFmtp = function(codec) {
  var line = '';
  var pt = codec.payloadType;
  if (codec.preferredPayloadType !== undefined) {
    pt = codec.preferredPayloadType;
  }
  if (codec.parameters && Object.keys(codec.parameters).length) {
    var params = [];
    Object.keys(codec.parameters).forEach(function(param) {
      if (codec.parameters[param]) {
        params.push(param + '=' + codec.parameters[param]);
      } else {
        params.push(param);
      }
    });
    line += 'a=fmtp:' + pt + ' ' + params.join(';') + '\r\n';
  }
  return line;
};

// Parses an rtcp-fb line, returns RTCPRtcpFeedback object. Sample input:
// a=rtcp-fb:98 nack rpsi
SDPUtils.parseRtcpFb = function(line) {
  var parts = line.substr(line.indexOf(' ') + 1).split(' ');
  return {
    type: parts.shift(),
    parameter: parts.join(' ')
  };
};
// Generate a=rtcp-fb lines from RTCRtpCodecCapability or RTCRtpCodecParameters.
SDPUtils.writeRtcpFb = function(codec) {
  var lines = '';
  var pt = codec.payloadType;
  if (codec.preferredPayloadType !== undefined) {
    pt = codec.preferredPayloadType;
  }
  if (codec.rtcpFeedback && codec.rtcpFeedback.length) {
    // FIXME: special handling for trr-int?
    codec.rtcpFeedback.forEach(function(fb) {
      lines += 'a=rtcp-fb:' + pt + ' ' + fb.type +
      (fb.parameter && fb.parameter.length ? ' ' + fb.parameter : '') +
          '\r\n';
    });
  }
  return lines;
};

// Parses an RFC 5576 ssrc media attribute. Sample input:
// a=ssrc:3735928559 cname:something
SDPUtils.parseSsrcMedia = function(line) {
  var sp = line.indexOf(' ');
  var parts = {
    ssrc: parseInt(line.substr(7, sp - 7), 10)
  };
  var colon = line.indexOf(':', sp);
  if (colon > -1) {
    parts.attribute = line.substr(sp + 1, colon - sp - 1);
    parts.value = line.substr(colon + 1);
  } else {
    parts.attribute = line.substr(sp + 1);
  }
  return parts;
};

SDPUtils.parseSsrcGroup = function(line) {
  var parts = line.substr(13).split(' ');
  return {
    semantics: parts.shift(),
    ssrcs: parts.map(function(ssrc) {
      return parseInt(ssrc, 10);
    })
  };
};

// Extracts the MID (RFC 5888) from a media section.
// returns the MID or undefined if no mid line was found.
SDPUtils.getMid = function(mediaSection) {
  var mid = SDPUtils.matchPrefix(mediaSection, 'a=mid:')[0];
  if (mid) {
    return mid.substr(6);
  }
};

SDPUtils.parseFingerprint = function(line) {
  var parts = line.substr(14).split(' ');
  return {
    algorithm: parts[0].toLowerCase(), // algorithm is case-sensitive in Edge.
    value: parts[1]
  };
};

// Extracts DTLS parameters from SDP media section or sessionpart.
// FIXME: for consistency with other functions this should only
//   get the fingerprint line as input. See also getIceParameters.
SDPUtils.getDtlsParameters = function(mediaSection, sessionpart) {
  var lines = SDPUtils.matchPrefix(mediaSection + sessionpart,
    'a=fingerprint:');
  // Note: a=setup line is ignored since we use the 'auto' role.
  // Note2: 'algorithm' is not case sensitive except in Edge.
  return {
    role: 'auto',
    fingerprints: lines.map(SDPUtils.parseFingerprint)
  };
};

// Serializes DTLS parameters to SDP.
SDPUtils.writeDtlsParameters = function(params, setupType) {
  var sdp = 'a=setup:' + setupType + '\r\n';
  params.fingerprints.forEach(function(fp) {
    sdp += 'a=fingerprint:' + fp.algorithm + ' ' + fp.value + '\r\n';
  });
  return sdp;
};
// Parses ICE information from SDP media section or sessionpart.
// FIXME: for consistency with other functions this should only
//   get the ice-ufrag and ice-pwd lines as input.
SDPUtils.getIceParameters = function(mediaSection, sessionpart) {
  var lines = SDPUtils.splitLines(mediaSection);
  // Search in session part, too.
  lines = lines.concat(SDPUtils.splitLines(sessionpart));
  var iceParameters = {
    usernameFragment: lines.filter(function(line) {
      return line.indexOf('a=ice-ufrag:') === 0;
    })[0].substr(12),
    password: lines.filter(function(line) {
      return line.indexOf('a=ice-pwd:') === 0;
    })[0].substr(10)
  };
  return iceParameters;
};

// Serializes ICE parameters to SDP.
SDPUtils.writeIceParameters = function(params) {
  return 'a=ice-ufrag:' + params.usernameFragment + '\r\n' +
      'a=ice-pwd:' + params.password + '\r\n';
};

// Parses the SDP media section and returns RTCRtpParameters.
SDPUtils.parseRtpParameters = function(mediaSection) {
  var description = {
    codecs: [],
    headerExtensions: [],
    fecMechanisms: [],
    rtcp: []
  };
  var lines = SDPUtils.splitLines(mediaSection);
  var mline = lines[0].split(' ');
  for (var i = 3; i < mline.length; i++) { // find all codecs from mline[3..]
    var pt = mline[i];
    var rtpmapline = SDPUtils.matchPrefix(
      mediaSection, 'a=rtpmap:' + pt + ' ')[0];
    if (rtpmapline) {
      var codec = SDPUtils.parseRtpMap(rtpmapline);
      var fmtps = SDPUtils.matchPrefix(
        mediaSection, 'a=fmtp:' + pt + ' ');
      // Only the first a=fmtp:<pt> is considered.
      codec.parameters = fmtps.length ? SDPUtils.parseFmtp(fmtps[0]) : {};
      codec.rtcpFeedback = SDPUtils.matchPrefix(
        mediaSection, 'a=rtcp-fb:' + pt + ' ')
        .map(SDPUtils.parseRtcpFb);
      description.codecs.push(codec);
      // parse FEC mechanisms from rtpmap lines.
      switch (codec.name.toUpperCase()) {
        case 'RED':
        case 'ULPFEC':
          description.fecMechanisms.push(codec.name.toUpperCase());
          break;
        default: // only RED and ULPFEC are recognized as FEC mechanisms.
          break;
      }
    }
  }
  SDPUtils.matchPrefix(mediaSection, 'a=extmap:').forEach(function(line) {
    description.headerExtensions.push(SDPUtils.parseExtmap(line));
  });
  // FIXME: parse rtcp.
  return description;
};

// Generates parts of the SDP media section describing the capabilities /
// parameters.
SDPUtils.writeRtpDescription = function(kind, caps) {
  var sdp = '';

  // Build the mline.
  sdp += 'm=' + kind + ' ';
  sdp += caps.codecs.length > 0 ? '9' : '0'; // reject if no codecs.
  sdp += ' UDP/TLS/RTP/SAVPF ';
  sdp += caps.codecs.map(function(codec) {
    if (codec.preferredPayloadType !== undefined) {
      return codec.preferredPayloadType;
    }
    return codec.payloadType;
  }).join(' ') + '\r\n';

  sdp += 'c=IN IP4 0.0.0.0\r\n';
  sdp += 'a=rtcp:9 IN IP4 0.0.0.0\r\n';

  // Add a=rtpmap lines for each codec. Also fmtp and rtcp-fb.
  caps.codecs.forEach(function(codec) {
    sdp += SDPUtils.writeRtpMap(codec);
    sdp += SDPUtils.writeFmtp(codec);
    sdp += SDPUtils.writeRtcpFb(codec);
  });
  var maxptime = 0;
  caps.codecs.forEach(function(codec) {
    if (codec.maxptime > maxptime) {
      maxptime = codec.maxptime;
    }
  });
  if (maxptime > 0) {
    sdp += 'a=maxptime:' + maxptime + '\r\n';
  }
  sdp += 'a=rtcp-mux\r\n';

  if (caps.headerExtensions) {
    caps.headerExtensions.forEach(function(extension) {
      sdp += SDPUtils.writeExtmap(extension);
    });
  }
  // FIXME: write fecMechanisms.
  return sdp;
};

// Parses the SDP media section and returns an array of
// RTCRtpEncodingParameters.
SDPUtils.parseRtpEncodingParameters = function(mediaSection) {
  var encodingParameters = [];
  var description = SDPUtils.parseRtpParameters(mediaSection);
  var hasRed = description.fecMechanisms.indexOf('RED') !== -1;
  var hasUlpfec = description.fecMechanisms.indexOf('ULPFEC') !== -1;

  // filter a=ssrc:... cname:, ignore PlanB-msid
  var ssrcs = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
    .map(function(line) {
      return SDPUtils.parseSsrcMedia(line);
    })
    .filter(function(parts) {
      return parts.attribute === 'cname';
    });
  var primarySsrc = ssrcs.length > 0 && ssrcs[0].ssrc;
  var secondarySsrc;

  var flows = SDPUtils.matchPrefix(mediaSection, 'a=ssrc-group:FID')
    .map(function(line) {
      var parts = line.substr(17).split(' ');
      return parts.map(function(part) {
        return parseInt(part, 10);
      });
    });
  if (flows.length > 0 && flows[0].length > 1 && flows[0][0] === primarySsrc) {
    secondarySsrc = flows[0][1];
  }

  description.codecs.forEach(function(codec) {
    if (codec.name.toUpperCase() === 'RTX' && codec.parameters.apt) {
      var encParam = {
        ssrc: primarySsrc,
        codecPayloadType: parseInt(codec.parameters.apt, 10)
      };
      if (primarySsrc && secondarySsrc) {
        encParam.rtx = {ssrc: secondarySsrc};
      }
      encodingParameters.push(encParam);
      if (hasRed) {
        encParam = JSON.parse(JSON.stringify(encParam));
        encParam.fec = {
          ssrc: primarySsrc,
          mechanism: hasUlpfec ? 'red+ulpfec' : 'red'
        };
        encodingParameters.push(encParam);
      }
    }
  });
  if (encodingParameters.length === 0 && primarySsrc) {
    encodingParameters.push({
      ssrc: primarySsrc
    });
  }

  // we support both b=AS and b=TIAS but interpret AS as TIAS.
  var bandwidth = SDPUtils.matchPrefix(mediaSection, 'b=');
  if (bandwidth.length) {
    if (bandwidth[0].indexOf('b=TIAS:') === 0) {
      bandwidth = parseInt(bandwidth[0].substr(7), 10);
    } else if (bandwidth[0].indexOf('b=AS:') === 0) {
      // use formula from JSEP to convert b=AS to TIAS value.
      bandwidth = parseInt(bandwidth[0].substr(5), 10) * 1000 * 0.95
          - (50 * 40 * 8);
    } else {
      bandwidth = undefined;
    }
    encodingParameters.forEach(function(params) {
      params.maxBitrate = bandwidth;
    });
  }
  return encodingParameters;
};

// parses http://draft.ortc.org/#rtcrtcpparameters*
SDPUtils.parseRtcpParameters = function(mediaSection) {
  var rtcpParameters = {};

  // Gets the first SSRC. Note tha with RTX there might be multiple
  // SSRCs.
  var remoteSsrc = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
    .map(function(line) {
      return SDPUtils.parseSsrcMedia(line);
    })
    .filter(function(obj) {
      return obj.attribute === 'cname';
    })[0];
  if (remoteSsrc) {
    rtcpParameters.cname = remoteSsrc.value;
    rtcpParameters.ssrc = remoteSsrc.ssrc;
  }

  // Edge uses the compound attribute instead of reducedSize
  // compound is !reducedSize
  var rsize = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-rsize');
  rtcpParameters.reducedSize = rsize.length > 0;
  rtcpParameters.compound = rsize.length === 0;

  // parses the rtcp-mux attrіbute.
  // Note that Edge does not support unmuxed RTCP.
  var mux = SDPUtils.matchPrefix(mediaSection, 'a=rtcp-mux');
  rtcpParameters.mux = mux.length > 0;

  return rtcpParameters;
};

// parses either a=msid: or a=ssrc:... msid lines and returns
// the id of the MediaStream and MediaStreamTrack.
SDPUtils.parseMsid = function(mediaSection) {
  var parts;
  var spec = SDPUtils.matchPrefix(mediaSection, 'a=msid:');
  if (spec.length === 1) {
    parts = spec[0].substr(7).split(' ');
    return {stream: parts[0], track: parts[1]};
  }
  var planB = SDPUtils.matchPrefix(mediaSection, 'a=ssrc:')
    .map(function(line) {
      return SDPUtils.parseSsrcMedia(line);
    })
    .filter(function(msidParts) {
      return msidParts.attribute === 'msid';
    });
  if (planB.length > 0) {
    parts = planB[0].value.split(' ');
    return {stream: parts[0], track: parts[1]};
  }
};

// SCTP
// parses draft-ietf-mmusic-sctp-sdp-26 first and falls back
// to draft-ietf-mmusic-sctp-sdp-05
SDPUtils.parseSctpDescription = function(mediaSection) {
  var mline = SDPUtils.parseMLine(mediaSection);
  var maxSizeLine = SDPUtils.matchPrefix(mediaSection, 'a=max-message-size:');
  var maxMessageSize;
  if (maxSizeLine.length > 0) {
    maxMessageSize = parseInt(maxSizeLine[0].substr(19), 10);
  }
  if (isNaN(maxMessageSize)) {
    maxMessageSize = 65536;
  }
  var sctpPort = SDPUtils.matchPrefix(mediaSection, 'a=sctp-port:');
  if (sctpPort.length > 0) {
    return {
      port: parseInt(sctpPort[0].substr(12), 10),
      protocol: mline.fmt,
      maxMessageSize: maxMessageSize
    };
  }
  var sctpMapLines = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:');
  if (sctpMapLines.length > 0) {
    var parts = SDPUtils.matchPrefix(mediaSection, 'a=sctpmap:')[0]
      .substr(10)
      .split(' ');
    return {
      port: parseInt(parts[0], 10),
      protocol: parts[1],
      maxMessageSize: maxMessageSize
    };
  }
};

// SCTP
// outputs the draft-ietf-mmusic-sctp-sdp-26 version that all browsers
// support by now receiving in this format, unless we originally parsed
// as the draft-ietf-mmusic-sctp-sdp-05 format (indicated by the m-line
// protocol of DTLS/SCTP -- without UDP/ or TCP/)
SDPUtils.writeSctpDescription = function(media, sctp) {
  var output = [];
  if (media.protocol !== 'DTLS/SCTP') {
    output = [
      'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.protocol + '\r\n',
      'c=IN IP4 0.0.0.0\r\n',
      'a=sctp-port:' + sctp.port + '\r\n'
    ];
  } else {
    output = [
      'm=' + media.kind + ' 9 ' + media.protocol + ' ' + sctp.port + '\r\n',
      'c=IN IP4 0.0.0.0\r\n',
      'a=sctpmap:' + sctp.port + ' ' + sctp.protocol + ' 65535\r\n'
    ];
  }
  if (sctp.maxMessageSize !== undefined) {
    output.push('a=max-message-size:' + sctp.maxMessageSize + '\r\n');
  }
  return output.join('');
};

// Generate a session ID for SDP.
// https://tools.ietf.org/html/draft-ietf-rtcweb-jsep-20#section-5.2.1
// recommends using a cryptographically random +ve 64-bit value
// but right now this should be acceptable and within the right range
SDPUtils.generateSessionId = function() {
  return Math.random().toString().substr(2, 21);
};

// Write boilder plate for start of SDP
// sessId argument is optional - if not supplied it will
// be generated randomly
// sessVersion is optional and defaults to 2
// sessUser is optional and defaults to 'thisisadapterortc'
SDPUtils.writeSessionBoilerplate = function(sessId, sessVer, sessUser) {
  var sessionId;
  var version = sessVer !== undefined ? sessVer : 2;
  if (sessId) {
    sessionId = sessId;
  } else {
    sessionId = SDPUtils.generateSessionId();
  }
  var user = sessUser || 'thisisadapterortc';
  // FIXME: sess-id should be an NTP timestamp.
  return 'v=0\r\n' +
      'o=' + user + ' ' + sessionId + ' ' + version +
        ' IN IP4 127.0.0.1\r\n' +
      's=-\r\n' +
      't=0 0\r\n';
};

SDPUtils.writeMediaSection = function(transceiver, caps, type, stream) {
  var sdp = SDPUtils.writeRtpDescription(transceiver.kind, caps);

  // Map ICE parameters (ufrag, pwd) to SDP.
  sdp += SDPUtils.writeIceParameters(
    transceiver.iceGatherer.getLocalParameters());

  // Map DTLS parameters to SDP.
  sdp += SDPUtils.writeDtlsParameters(
    transceiver.dtlsTransport.getLocalParameters(),
    type === 'offer' ? 'actpass' : 'active');

  sdp += 'a=mid:' + transceiver.mid + '\r\n';

  if (transceiver.direction) {
    sdp += 'a=' + transceiver.direction + '\r\n';
  } else if (transceiver.rtpSender && transceiver.rtpReceiver) {
    sdp += 'a=sendrecv\r\n';
  } else if (transceiver.rtpSender) {
    sdp += 'a=sendonly\r\n';
  } else if (transceiver.rtpReceiver) {
    sdp += 'a=recvonly\r\n';
  } else {
    sdp += 'a=inactive\r\n';
  }

  if (transceiver.rtpSender) {
    // spec.
    var msid = 'msid:' + stream.id + ' ' +
        transceiver.rtpSender.track.id + '\r\n';
    sdp += 'a=' + msid;

    // for Chrome.
    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
        ' ' + msid;
    if (transceiver.sendEncodingParameters[0].rtx) {
      sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
          ' ' + msid;
      sdp += 'a=ssrc-group:FID ' +
          transceiver.sendEncodingParameters[0].ssrc + ' ' +
          transceiver.sendEncodingParameters[0].rtx.ssrc +
          '\r\n';
    }
  }
  // FIXME: this should be written by writeRtpDescription.
  sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].ssrc +
      ' cname:' + SDPUtils.localCName + '\r\n';
  if (transceiver.rtpSender && transceiver.sendEncodingParameters[0].rtx) {
    sdp += 'a=ssrc:' + transceiver.sendEncodingParameters[0].rtx.ssrc +
        ' cname:' + SDPUtils.localCName + '\r\n';
  }
  return sdp;
};

// Gets the direction from the mediaSection or the sessionpart.
SDPUtils.getDirection = function(mediaSection, sessionpart) {
  // Look for sendrecv, sendonly, recvonly, inactive, default to sendrecv.
  var lines = SDPUtils.splitLines(mediaSection);
  for (var i = 0; i < lines.length; i++) {
    switch (lines[i]) {
      case 'a=sendrecv':
      case 'a=sendonly':
      case 'a=recvonly':
      case 'a=inactive':
        return lines[i].substr(2);
      default:
        // FIXME: What should happen here?
    }
  }
  if (sessionpart) {
    return SDPUtils.getDirection(sessionpart);
  }
  return 'sendrecv';
};

SDPUtils.getKind = function(mediaSection) {
  var lines = SDPUtils.splitLines(mediaSection);
  var mline = lines[0].split(' ');
  return mline[0].substr(2);
};

SDPUtils.isRejected = function(mediaSection) {
  return mediaSection.split(' ', 2)[1] === '0';
};

SDPUtils.parseMLine = function(mediaSection) {
  var lines = SDPUtils.splitLines(mediaSection);
  var parts = lines[0].substr(2).split(' ');
  return {
    kind: parts[0],
    port: parseInt(parts[1], 10),
    protocol: parts[2],
    fmt: parts.slice(3).join(' ')
  };
};

SDPUtils.parseOLine = function(mediaSection) {
  var line = SDPUtils.matchPrefix(mediaSection, 'o=')[0];
  var parts = line.substr(2).split(' ');
  return {
    username: parts[0],
    sessionId: parts[1],
    sessionVersion: parseInt(parts[2], 10),
    netType: parts[3],
    addressType: parts[4],
    address: parts[5]
  };
};

// a very naive interpretation of a valid SDP.
SDPUtils.isValidSDP = function(blob) {
  if (typeof blob !== 'string' || blob.length === 0) {
    return false;
  }
  var lines = SDPUtils.splitLines(blob);
  for (var i = 0; i < lines.length; i++) {
    if (lines[i].length < 2 || lines[i].charAt(1) !== '=') {
      return false;
    }
    // TODO: check the modifier a bit more.
  }
  return true;
};

// Expose public methods.
if (typeof module === 'object') {
  module.exports = SDPUtils;
}

},{}]},{},[1])(1)
});

 /**
 * Created by Gifer on 03.02.2016.
 */

 "use strict";

 function CallStates(){
     var current_state       = 0;   // текущее состояние
     var universalStateEvent = null;
     var Self = this;

     function isArray(obj){
         return Object.prototype.toString.call( obj ) === '[object Array]';
     }

     this.list = {
         free                               : 0,   // свободен

         server_state_try                   : 1,   // попытка звонка
         server_state_rejected              : 2,   // от звонка отказались
         server_state_rejected_os_too_old   : 3,   // звонок невозможен, ОС слишком старая
         server_state_accepted              : 4,   // звонок принят
         server_state_cancelled             : 5,   // попытка звонка отменена
         server_state_cancelled_by_timeout  : 6,   // попытка звонка не состоялась, абонент не ответил
         server_state_error                 : 7,   // возникла ошибка при звонке
         server_state_started               : 8,   // звонок успешно начался
         server_state_closed                : 9,   // звонок закончен
         server_state_busy                  : 10,  // абонент занят
         
         outgoing_call_try                  : 1,   // попытка исходящего звонка
         outgoing_call_busy                 : 2,   // исходящий звонок, абонент занят
         outgoing_call_rejected             : 3,   // абонент отказался принять ваш звонок
         outgoing_call_accepted             : 4,   // абонент принял ваш звонок
         outgoing_call_cancelled            : 5,   // отбой исходящего звонка мной, я передумал
         outgoing_call_cancelled_by_timeout : 6,   // исходящий звонок не состоялся, абонент не ответил по тайм-ауту
         outgoing_call_remote_error         : 7,   // какая-то ошибка при исходящем звонке, на удалённой стороне
         outgoing_call_my_error             : 8,   // какая-то ошибка при исходящем звонке, на моей стороне
         outgoing_call_started              : 9,   // исходящий звонок успешно начался
         outgoing_call_my_close             : 10,  // я завершил исходящий звонок
         outgoing_call_remote_close         : 11,  // мой исходящий звонок был завершен удалённой стороной
         outgoing_call_rejected_user_not_in_contacts : 12,  // Пользователь отказался принять звонок, вас нет в его контактах

         incoming_call_try                  : 101, //+ попытка входящего звонка
         incoming_call_busy                 : 102, // входящий звонок невозможен, у меня занято
         incoming_call_rejected             : 103, // я отказался от входящего звонка
         incoming_call_accepted             : 104, // я согласился принять входящий звонок
         incoming_call_cancelled            : 105, // удалённая сторона досрочно отказалась от звонка, я не спел согласиться или отказаться
         incoming_call_cancelled_by_timeout : 106, // входящий звонок не состоялся, я не ответил по тайм-ауту
         incoming_call_remote_error         : 107, // какая-то ошибка при входящем звонке, на удалённой стороне
         incoming_call_my_error             : 108, // какая-то ошибка при входящем звонке, на моей стороне
         incoming_call_started              : 109, // входящий звонок успешно начался
         incoming_call_my_close             : 110, // я завершил входящий звонок
         incoming_call_remote_close         : 111  // вхолящий звонок завершил удалённый пользователь
     };

     this.setStateEvent = function(state, args){
         current_state = state;

         console.log('State changed to: ' + state);

         if (universalStateEvent){
             universalStateEvent.apply(current_state, isArray(args) ? args : [args]);
         }
     };

     this.setUniversalStateEvent = function(event){
         universalStateEvent = event;
     };

     this.currentState = function(){
         return current_state;
     };

     this.setCustomCallEvent = function(state/*, event*/){
         current_state = state;

         console.log('State changed to: ' + state);
     };

     this.clearState = function(){
         current_state   = Self.list.free;
         Self.isMyCall   = false;

         console.log('Clear State: ' + current_state);
     };

     this.isMyCall = false;
 }

 /**
 * Created by Gifer on 28.01.2016.
 */

 function MediaCall(){
     "use strict";

     var Self      = this;
     var myStream  = null;
     var pcList    = {};
     var pcStreams = {};
     var calleeUIN = null;
     var failTimer = null;
     var dialingTimer = null;
     var pc        = null;
     var mirrorContainer = null;
     var calleeContainer = null;
     var devices   = {

     };
     var deviceList= {};
     var iceRestart= false;
     var isWeb     = location.protocol !== "file:";
     var isMyCall  = false;

     var CALLTYPE = {
         VOICE  : 0,
         VIDEO  : 1,
         SHARING: 2
     };
     var recordOpt = {
         mimeType: 'video/webm; codecs=vp8'
     };
     var getDefaultConfig  = function () {
         return {
             iceTransportPolicy : "all",
             iceTransport : "all",
             iceServers : []
         }
     };
     var configuration  = getDefaultConfig();
     var streamBackup   = null;
     var streamAssigned = false;
     var isDisconnectICE= false;
     var screenID       = "screen:0:0";
     var callType       = CALLTYPE.VOICE;

     var callStreams  = {
         income : null,
         my     : null,
         get    : function () {
             var res = [];

             if (callStreams.income){
                 res.push(callStreams.income);
             }

             if (callStreams.my){
                 res.push(callStreams.my);
             }

             return res;
         }
     }

     // ===========================================================

     this.onFailedInit  = null;
     this.onError       = null;
     this.sendAnswer    = null;
     this.sendOffer     = null;
     this.sendCandidate = null;
     this.showMedia     = null;
     this.iceClosed     = null;
     this.onDisconnected   = null;
     this.onRestoreConnect = null;

     this.restoreCall   = true;
     this.useVideo      = false;
     this.MID           = -1;
     this.initFailTime  = 95000;
     this.Share         = false;
     this.receiveScreen = false;

     // ===========================================================

     function getUserMedia(opt, ok, err) {
         if (navigator.getUserMedia) {
             navigator.getUserMedia(opt, ok, err);
         } else
         if (navigator.mediaDevices && (typeof navigator.mediaDevices.getUserMedia !== 'undefined')) {
             navigator.mediaDevices.getUserMedia(opt).then(ok).catch(err);
         } else {
             console.error("Hasn't getUserMedia!");
         }
     }

     function isArray(obj){
         return Object.prototype.toString.call( obj ) === '[object Array]';
     }

     function isObject(obj) {
         return Object.prototype.toString.call( obj ) === '[object Object]';
     }

     function onIceStateChange(__pc) { // event
         if (__pc) {
             if ( __pc.iceConnectionState === 'connected' || __pc.iceConnectionState === 'completed') {
                 __pc.getStats(null).then(function(results) {
                     // figure out the peer's ip
                     var activeCandidatePair = null;
                     var remoteCandidate = null;

                     // search for the candidate pair
                     Object.keys(results).forEach(function(result) {
                         var report = results[result];

                         if (report.type === 'candidatepair' && report.selected || report.type === 'googCandidatePair' &&
                             report.googActiveConnection === 'true') {
                             activeCandidatePair = report;
                         }
                     });

                     if (activeCandidatePair && activeCandidatePair.remoteCandidateId) {
                         Object.keys(results).forEach(function(result) {
                             var report = results[result];

                             if (report.type === 'remotecandidate' && report.id === activeCandidatePair.remoteCandidateId) {
                                 remoteCandidate = report;
                             }
                         });
                     }

                     if (remoteCandidate && remoteCandidate.ipAddress && remoteCandidate.portNumber) {
                         console.log('[onIceStateChange]: Remote address changed to ' + remoteCandidate.ipAddress + ":" + remoteCandidate.portNumber);
                     }
                 });
             }
         }
     }
     
     function assignStream(callee) {
         if (streamBackup && streamBackup.stream){
             var videoContainer = calleeContainer || document.createElement('video');

             videoContainer.className = "VideoCaller";
             videoContainer.autoplay  = true;
             videoContainer.srcObject = streamBackup.stream;

             videoContainer.onloadedmetadata = function (e) {
                 videoContainer.play();
             };

             if (typeof videoContainer.sinkId !== 'undefined' && devices.sound) {
                 setTimeout(function () {
                     videoContainer.setSinkId(devices.sound).then(function () {
                         console.log("Set sound device: " + devices.sound);
                     }).catch(function (error) {
                         console.error(error.message);
                     });
                 }, 10);
             }/* else {
                 console.log('NO sinkId!');
             }*/
             
             pcStreams[callee] = videoContainer;

             streamAssigned = true;
         }

         console.log('AssignStream iceConnectionState: ' + pc.iceConnectionState);
         console.log(streamBackup);

         // window.___callStream = streamBackup;  // todo: remove
     }

     function offer(callee, restart) {
         pc.createOffer({
             offerToReceiveAudio: true,
             offerToReceiveVideo: true,
             iceRestart         : restart || iceRestart
         }).then( function (desc) {
             pc.setLocalDescription(desc);

             Self.sendOffer(callee, Self.MID, JSON.stringify(desc));
         }, function(err){
             Self.hangUp('CreateOffer: ' + JSON.stringify(err));
         });
     }

     function recall(callee, __pc, restart, _now) {
         if (dialingTimer) {
             clearTimeout(dialingTimer);
         }

         if (restart){
             dialingTimer = setTimeout(function () {
                 dialingTimer = null;

                 if (__pc.iceConnectionState === 'checking' || __pc.iceConnectionState === 'failed'){ // disconnected
                     console.log("Recall on IceConnectionState:" + __pc.iceConnectionState);

                     offer(callee, restart);
                 } else {
                     console.log("Start recall");

                     recall(callee, __pc, !restart); // true
                 }
             }, _now ? 0 : 4000);
         }
     }

     function getScreenThumbs(cb) {
         if (!isWeb) {
             var desktopCapturer = require('electron').desktopCapturer;
             var res = {};

             desktopCapturer.getSources({ types: ['screen'] })
             .then(function (sources) {
                 for (var source in sources) {
                     var img = document.createElement("img");
                     img.src = source.thumbnail.toDataURL();

                     res[source.id] = img;
                 }
                 
                 cb(res);
             });
         }
     }
     
     function gotDevices(allDevices) {
         deviceList = {
             audioinput  : [],
             audiooutput : [],
             videoinput  : []
         };

         for (var i = 0; i !== allDevices.length; ++i) {
             var deviceInfo = allDevices[i];

             if (deviceList[deviceInfo.kind]) {
                 deviceList[deviceInfo.kind].push({
                     deviceId : deviceInfo.deviceId,
                     label    : deviceInfo.label
                 });
             }
         }

         console.log(deviceList);
     }

     function handleError(error) {
         console.log('navigator.getUserMedia error: ', error);
     }

     function startCall(callee, sdp) {
         var createOffer = !sdp;

         isDisconnectICE = false;

         pc = pcList[callee] || new RTCPeerConnection(configuration);

         console.log("Start call config: " + JSON.stringify(configuration));

         if (!pcList[callee]) {
             pcList[callee] = pc;

             // navigator.mediaDevices.enumerateDevices().then(gotDevices).catch(handleError);

             pc.oniceconnectionstatechange = function(evt) {
                 console.log("ICE connection state change: " + evt.target.iceConnectionState);

                 if (pc.iceConnectionState === 'disconnected') {
                     iceRestart = Self.restoreCall;

                     isDisconnectICE = true;

                     if (iceRestart) {
                         recall(callee, pc, iceRestart, true);
                     } else
                     if (Self.onDisconnected) {
                         Self.onDisconnected(callee);
                     }
                 } else
                 if (pc.iceConnectionState === 'closed' && Self.iceClosed) {
                     Self.iceClosed();
                 } else

                 if (pc.iceConnectionState === 'failed'){
                     iceRestart = Self.restoreCall;

                     if (iceRestart) {
                         recall(callee, pc, iceRestart, true);
                     } else {
                         runError('Error: ICE Connection State FAILED!', 1);
                     }
                 } else

                 if ((pc.iceConnectionState === 'connected' || pc.iceConnectionState === 'completed') && Self.showMedia){
                     clearTimeout(failTimer);
                     clearTimeout(dialingTimer);

                     if (isDisconnectICE){
                         isDisconnectICE = false;

                         if (Self.onRestoreConnect){
                             Self.onRestoreConnect(callee);
                         }
                     }

                     dialingTimer = null;
                     failTimer    = null;

                     onIceStateChange(pc, evt);

                     if (!iceRestart) {
                         callStreams.income = streamBackup && streamBackup.stream ? streamBackup.stream : null;
                         callStreams.my     = myStream;

                         Self.showMedia.apply(null, [pcStreams[callee], streamBackup, myStream]);
                     }

                     if (!streamAssigned){
                         assignStream(callee);
                     }
                 }
             };

             pc.onicecandidateerror = function(err) {
                 //runError('OnIceCandidateError: ' + JSON.stringify(err), 1);
                 console.log('OnIceCandidateError (code:' + err.errorCode + ") " + err.errorText);
                 console.log('hostCandidate: ' + err.hostCandidate);
                 console.log('url ' + err.url);
             };

             pc.onicecandidate = function (evt) { // send any ice candidates to the other peer
                 //console.log("ICE gathering state change: " + evt.target.iceGatheringState);

                 if (evt.candidate && evt.candidate.candidate.indexOf(' tcp ') === -1) {
                     console.log("OUTGOING ICE: " + evt.candidate.candidate);

                     Self.sendCandidate(callee, Self.MID, JSON.stringify(evt.candidate));
                 }
             };

             pc.onerror = function(err) {
                 console.log(err);
             };

             pc.onaddstream = function (evt) {
                 streamBackup = evt;

                 console.log('Get Remote Stream complete!');
                 // console.log(streamBackup);
             };

             if (Self.useVideo){
             // if (callType === CALLTYPE.VIDEO){
                 if (Object.prototype.toString.call( mirrorContainer ) === '[object String]'){
                     mirrorContainer = document.getElementById(mirrorContainer);
                 }

                 if (['[object HTMLVideoElement]', '[object HTMLAudioElement]'].indexOf(Object.prototype.toString.call( mirrorContainer )) === -1){
                     // console.error('\"mirrorContainer\" is not a HTML element!');

                     Self.hangUp('\"mirrorContainer\" is not a HTML element!');

                     return false;
                 } else
                 if (myStream){
                     mirrorContainer.srcObject = myStream;
                 }
             }

             if (myStream) pc.addStream(myStream);
         }

         console.log("ICE  GatheringState: " + pc.iceGatheringState);
         console.log("ICE ConnectionState: " + pc.iceConnectionState);

         if (createOffer){
             offer(callee);

             recall(callee, pc, true, false);
         } else {
             if (!isObject(sdp)){
                 sdp = JSON.parse(sdp);
             }

             pc.setRemoteDescription(new RTCSessionDescription(sdp));

             pc.createAnswer(function (desc) {
                 pc.setLocalDescription(desc);

                 Self.sendAnswer(callee, Self.MID, JSON.stringify(desc));
             }, function(err){
                 Self.hangUp('CreateAnswer: ' + JSON.stringify(err));
             });
         }

         return pc;
     }

     function runError() {
         clearTimeout(failTimer);

         if (Self.onError){
             Self.onError.apply(null, arguments);
         }
     }

     function getScreen(nextStep) {
         if (!isWeb) {
             getUserMedia({
                 audio : false,
                 video : {
                     mandatory: {
                         chromeMediaSource  : 'desktop',
                         chromeMediaSourceId: screenID, // "screen:0:0",
                         minWidth           : 160,
                         minHeight          : 120,
                         maxWidth           : 1920,
                         maxHeight          : 1080,
                         minFrameRate       : 5,
                         maxFrameRate       : 10
                     }
                 }
             }, function successGetMedia(screenStream) {
                 // console.log("GetMedia screen: " + JSON.stringify(shareStream));
                 if (!myStream) {
                     myStream = screenStream;
                 } else {
                     myStream.addTrack(screenStream.getVideoTracks()[0]);
                 }

                 var tracks = screenStream.getVideoTracks();

                 if (tracks && tracks[0]) {
                     tracks[0].enabled = !!Self.Share; // callType === CALLTYPE.SHARING && isMyCall; //!!Self.Share;
                 }

                 nextStep();
             }, function errorGetMedia(err){
                 console.log('Error getting screen stream: ' + JSON.stringify(err));

                 if (myStream){
                     nextStep();
                 } else {
                     Self.hangUp("No media devices");
                 }
             });
         } else {
             if (myStream){
                 nextStep();
             } else {
                 Self.hangUp("No media devices");
             }
         }
     }

     function GetMedia(cb){
         if (!myStream){
             var opt = {
                 "audio": {
                     optional: [
                         { echoCancellation: true },
                         { noiseSuppression: true },
                         { autoGainControl: true },
                         { googEchoCancellation: true },
                         { googEchoCancellation2: true },
                         { googNoiseSuppression: true },
                         { googNoiseSuppression2: true },
                         { googAutoGainControl: true },
                         { googAutoGainControl2: true },
                         { googHighpassFilter: true },
                         { googAudioMirroring: false },
                     ].concat(devices && devices.microphone ? {sourceId: devices.microphone} : [])
                 },
                 "video": Self.useVideo //callType === CALLTYPE.VIDEO //
                     ? {
                         mandatory: {
                             minWidth : 640,
                             minHeight: 480,

                             maxWidth : 1280,
                             maxHeight: 720,

                             minAspectRatio: 4/3,
                             maxAspectRatio: 16/9,

                             minFrameRate  : 15,
                             maxFrameRate  : 25
                         },
                         optional: [].concat(/*callType === CALLTYPE.VIDEO*/ Self.useVideo && devices.camera ? { sourceId: devices.camera } : [])
                     } 
                     : false
             };

             console.log('GetMedia config: ' + JSON.stringify(opt));

             if (!failTimer){
                 failTimer = setTimeout(function () {
                     failTimer = null;

                     if (Self.onFailedInit){
                         Self.onFailedInit();
                     }

                     Self.hangUp("initFailTime");
                 }, Self.initFailTime);
             }

             getUserMedia(opt, function successGetMedia(stream) {
                 console.log('getUserMedia: Media stream get successfully');

                 myStream = stream;

                 if (!myStream){
                     console.log('Getting empty stream!');
                 }

                 getScreen(cb);
             }, function errorGetMedia(err){
                 myStream = new MediaStream();

                 if (err.name === "NotFoundError" && Self.Share){
                     getScreen(cb);
                 } else
                 if (isMyCall){
                     console.error("Error getting media stream: hasn't camera or microphone");

                     cb();
                 }
                 else {
                     cb();
                 }
             })
         } else {
             cb();
         }
     }

     // ===========================================================

     this.getScreenThumbs = getScreenThumbs;

     this.setScreenId   = function (id){
         screenID = id || "screen:0:0";
     };

     this.addIceServers = function (srv, iceTransportPolicy){
         configuration.iceServers         = srv || [];
         configuration.iceTransportPolicy = iceTransportPolicy || "all";
         configuration.iceTransport       = iceTransportPolicy || "all";
     };

     this.setCallInfo   = function (_callType/*video, share*/, MID) {
         callType = parseInt(_callType);
         Self.useVideo       = callType === CALLTYPE.VIDEO;
         Self.receiveScreen  = callType === CALLTYPE.SHARING;
         Self.MID            = MID;
     };

     this.setDevices    = function (microphome, camera, sound) {
         if (isObject(microphome)){
             devices = {
                 microphone : microphome.microphome,
                 camera     : microphome.camera,
                 sound      : microphome.sound
                 // sound      : "574c823d189c645f65f6bf51105b970b70e32b37dbc14d45e1ad285d52089bba" // microphome.sound
                 // sound      : "514edac8a4f360b1f3afdda7a5f77907e8c2132e2dcb5dc44fde0584433b8978" // microphome.sound
             };
         } else {
             devices = {
                 microphone : microphome,
                 camera     : camera,
                 sound      : sound
                 // sound      : "574c823d189c645f65f6bf51105b970b70e32b37dbc14d45e1ad285d52089bba" //sound
                 // sound      : "514edac8a4f360b1f3afdda7a5f77907e8c2132e2dcb5dc44fde0584433b8978" //sound
             };
         }

         console.log("DEVICES:" + JSON.stringify(devices));
     };

     this.setMirror     = function (target){
         mirrorContainer = target;
     };

     this.setCallee     = function (target){
         if (Object.prototype.toString.call( target ) === '[object String]'){
             calleeContainer = document.getElementById(target);
         } else {
             calleeContainer = target;
         }

         if (['[object HTMLVideoElement]', '[object HTMLAudioElement]'].indexOf(Object.prototype.toString.call( calleeContainer )) === -1){
             Self.hangUp('\"calleeContainer\" is not a HTML element!');
         }
     };

     this.recvAnswer    = function (data){
         console.log('RECEIVE ANSWER');

         if (!isObject(data.SDP)){
             data.SDP = JSON.parse(data.SDP);
         }

         if (pcList[data.UIN] && pcList[data.UIN].setRemoteDescription){
             pcList[data.UIN].setRemoteDescription(new RTCSessionDescription(data.SDP));
         } else {
             Self.hangUp('error in "RECEIVE ANSWER"');
         }
     };

     this.recvOffer     = function (data){
         console.log('RECEIVE OFFER');

         isMyCall = false;

         GetMedia(function(){
             startCall(data.UIN, data.SDP);
         });
     };

     this.recvCandidate = function (data){
         console.log('RECEIVE CANDIDATE');

         if (pcList[data.UIN] && pcList[data.UIN].addIceCandidate) {
             if (!isObject(data.CANDIDATE)){
                 data.CANDIDATE = JSON.parse(data.CANDIDATE);
             }

             if (configuration.iceTransportPolicy === "all" || data.CANDIDATE.candidate.indexOf("typ relay") > 0) {
                 pcList[data.UIN].addIceCandidate(new RTCIceCandidate(data.CANDIDATE));

                 console.log("INCOMING ICE:" + JSON.stringify(data.CANDIDATE));
             } else {
                 console.log("IGNORING LOCAL ICE:" + JSON.stringify(data.CANDIDATE));
             }
         }
     };

     // === START CALL ===
     this.recvReady     = function (list){
         console.log('RECEIVE READY');

         pc = null;

         calleeUIN = list || calleeUIN;

         if (calleeUIN){
             isMyCall = true;

             GetMedia(function(){
                 if (calleeUIN){
                     if (!isArray(calleeUIN)){
                         calleeUIN = calleeUIN.toString().split(',');
                     }

                     console.log('Call to: ' + calleeUIN);
                     
                     startCall(calleeUIN[0]);
                 }
             });
         }
     };

     this.restartIceNow = function () {
         iceRestart = true;

         startCall(isArray(calleeUIN) ? calleeUIN[0] : calleeUIN);
     };

     this.CallToUser    = function (uin){
         calleeUIN = uin;
     };

     this.switchCamera  = function () {
         var stream = myStream ? myStream.getVideoTracks() : [];

         if (stream.length) {
             stream[0].enabled = !stream[0].enabled;
         }

         return stream && stream[0] ? stream[0].enabled : false;
     };

     this.switchMicrophone = function () {
         var stream = myStream ? myStream.getAudioTracks() : [];

         if (stream.length) {
             stream[0].enabled = !stream[0].enabled;
         }

         return stream && stream[0] ? stream[0].enabled : false;
     };

     this.getCallType   = function (){
         return callType;
     };

     this.getCallStreams = function (){
         return callStreams.get();
     }

     this.hangUp        = function (error){
         console.log('Hang Up Call');

         if (myStream) {
             try {
                 mirrorContainer.pause();
                 mirrorContainer.srcObject = null;
             } catch (e){}

             if (myStream.getTracks) {
                 myStream.getTracks().forEach(function(track){
                     track.stop();
                 });

                 try {
                     myStream.stop();
                 } catch (e) {}
             }
         } else {
             if (Self.iceClosed) {
                 Self.iceClosed();
             }
         }

         for (var i in pcStreams){
         }

         try {
             pcStreams[i].pause();
             pcStreams[i].srcObject.getTracks().forEach(function(track){
                 track.stop();

             });
         } catch (e){}

         for (var i in pcList){
             pcList[i].close();
             pcList[i] = null;
         }

         pcList    = {};
         pcStreams = {};
         myStream  = null;
         calleeUIN = null;
         Self.MID  = -1;

         try {
             pc && pc.close();
         } catch (e){}

         streamBackup   = null;
         streamAssigned = false;
         iceRestart     = false;

         if (calleeContainer){
             calleeContainer.srcObject = null;
         }

         clearTimeout(failTimer);
         failTimer = null;

         if (error){
             runError(error);
         }

         isMyCall = false;

         configuration = getDefaultConfig();
     };
 }

function mcPlaySound($rootScope){
    "use strict";

    var isBrowser = location.protocol !== 'file:';

    var Sounds    = {
        Call      : 1,
        Receive   : 2,
        Reject    : 3,
        Busy      : 4,
        End       : 5,
        Private   : 6,
        ConfMsg   : 7,
        JoinConf  : 8,
        LeaveConf : 9,
        EnterBtn  : 10,
        Typing    : 11,
        DeleteAct : 12,
        BBS       : 13,
        BEEP      : 14
    };

    var SoundList = {};

    var self = this;

    var nowPlaying = null;

    this.snd = Sounds;
    this.Sounds = Sounds;

    function _play(idx) {
        nowPlaying = idx;

        try {
            SoundList[idx].play().catch(function () {});
            SoundList[idx].onended = SoundList[idx].onended || function () {
                this.onended = null;
                
                if (this.sndIdx === nowPlaying) nowPlaying = null;
            };
            SoundList[nowPlaying].onpause = SoundList[nowPlaying].onpause || function () {
                this.onpause = null;

                if (this.sndIdx === nowPlaying) nowPlaying = null;
            };
        } catch (e){}
    }

    this.Play = function(idx, once){
        if ($rootScope.isWebClient || !mcConst.ClientSettings.SoundsAllSoundsOff)
        if (SoundList[idx]){
            if (once){
                SoundList[idx].currentTime = 0;
            } else

            if (nowPlaying){
                self.Stop(idx);
            }

            _play(idx);
        } else {
            console.warn("Sound idx: " + idx + " is not defined");
        }
    };

    this.PlaySound = this.Play;

    this.Stop = function(idx){
        if (idx) {
            if (SoundList[idx]){
                try {
                    SoundList[idx].pause();
                    SoundList[idx].currentTime = 0;
                } catch (e){}
            } else {
                console.warn("Sound idx: " + idx + " is not defined");
            }
        } else {
            for (var sound in SoundList) {
                if (SoundList[sound]){
                    try {
                        SoundList[sound].pause();
                        SoundList[sound].currentTime = 0;
                    } catch (e){}
                }
            }
        }
    };

    function init() {
        var html = "";

        if (isBrowser){ // Web Client
            html =
                '<audio id="Sound_Ring"      preload loop><source src="source/media/mediaincomingcall.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_Calling"   preload loop><source src="source/media/mediacall.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_End"       preload><source src="source/media/mediaendcall.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_Reject"    preload><source src="source/media/mediacallreject.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_Busy"      preload><source src="source/media/mediabusy.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_Private"   preload><source src="source/media/private.mp3" type="audio/mpeg"></audio>' +
                '<audio id="Sound_EnterBtn"  preload><source src="source/media/chatret.mp3" type="audio/mpeg"></audio>' +
                '<audio id="Sound_Typing"    preload><source src="source/media/chattype.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_JoinConf"  preload><source src="source/media/join.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_ConfMsg"   preload><source src="source/media/chat.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_DeleteAct" preload><source src="source/media/chatbs.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_LeaveConf" preload><source src="source/media/leave.mp3" type="audio/mpeg"></audio>' +
                '<audio id="Sound_BEEP"      preload><source src="source/media/signal.mp3" type="audio/mpeg"></audio>';
        } else { // Electron Client
            
            var profile = require('../service/profile');
            var pathToSounds = profile.getProfilePath() + '/sounds/';

            html =
                '<audio id="Sound_BBS"       preload><source src="' + pathToSounds + 'newmsg.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_Ring"      preload loop><source src="' + pathToSounds + 'mediaincomingcall.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_Calling"   preload loop><source src="' + pathToSounds + 'mediacall.mp3"  type="audio/mpeg"></audio>'+
                '<audio id="Sound_End"       preload><source src="' + pathToSounds + 'mediaendcall.mp3"    type="audio/mpeg"></audio>'+
                '<audio id="Sound_Reject"    preload><source src="' + pathToSounds + 'mediacallreject.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_Busy"      preload><source src="' + pathToSounds + 'mediabusy.mp3" type="audio/mpeg"></audio>'+
                '<audio id="Sound_ConfMsg"   preload><source src="' + pathToSounds + 'chat.mp3"      type="audio/mpeg"></audio>'+
                '<audio id="Sound_JoinConf"  preload><source src="' + pathToSounds + 'join.mp3"      type="audio/mpeg"></audio>'+
                '<audio id="Sound_LeaveConf" preload><source src="' + pathToSounds + 'leave.mp3"     type="audio/mpeg"></audio>'+
                '<audio id="Sound_EnterBtn"  preload><source src="' + pathToSounds + 'chatret.mp3"   type="audio/mpeg"></audio>'+
                '<audio id="Sound_Typing"    preload><source src="' + pathToSounds + 'chattype.mp3"  type="audio/mpeg"></audio>'+
                '<audio id="Sound_DeleteAct" preload><source src="' + pathToSounds + 'chatbs.mp3"    type="audio/mpeg"></audio>'+
                '<audio id="Sound_Private"   preload><source src="' + pathToSounds + 'private.mp3"   type="audio/mpeg"></audio>'+
                '<audio id="Sound_BEEP"      preload><source src="' + pathToSounds + 'signal.mp3"    type="audio/mpeg"></audio>';
        }

        document.body.insertAdjacentHTML('beforeEnd', html);

        SoundList[Sounds.Call]      = document.getElementById("Sound_Ring");
        SoundList[Sounds.Receive]   = document.getElementById("Sound_Calling");
        SoundList[Sounds.Reject]    = document.getElementById("Sound_End");
        SoundList[Sounds.Busy]      = document.getElementById("Sound_Reject");
        SoundList[Sounds.End]       = document.getElementById("Sound_Busy");
        SoundList[Sounds.Private]   = document.getElementById("Sound_Private");
        SoundList[Sounds.JoinConf]  = document.getElementById("Sound_JoinConf");
        SoundList[Sounds.LeaveConf] = document.getElementById("Sound_LeaveConf");
        SoundList[Sounds.EnterBtn]  = document.getElementById("Sound_EnterBtn");
        SoundList[Sounds.Typing]    = document.getElementById("Sound_Typing");
        SoundList[Sounds.ConfMsg]   = document.getElementById("Sound_ConfMsg");
        SoundList[Sounds.DeleteAct] = document.getElementById("Sound_DeleteAct");
        SoundList[Sounds.BEEP]      = document.getElementById("Sound_BEEP");

        SoundList[Sounds.Call].sndIdx      = Sounds.Call;
        SoundList[Sounds.Receive].sndIdx   = Sounds.Receive;
        SoundList[Sounds.Reject].sndIdx    = Sounds.Reject;
        SoundList[Sounds.Busy].sndIdx      = Sounds.Busy;
        SoundList[Sounds.End].sndIdx       = Sounds.End;
        SoundList[Sounds.Private].sndIdx   = Sounds.Private;
        SoundList[Sounds.JoinConf].sndIdx  = Sounds.JoinConf;
        SoundList[Sounds.LeaveConf].sndIdx = Sounds.LeaveConf;
        SoundList[Sounds.EnterBtn].sndIdx  = Sounds.EnterBtn;
        SoundList[Sounds.Typing].sndIdx    = Sounds.Typing;
        SoundList[Sounds.ConfMsg].sndIdx   = Sounds.ConfMsg;
        SoundList[Sounds.DeleteAct].sndIdx = Sounds.DeleteAct;
        SoundList[Sounds.BEEP].sndIdx      = Sounds.BEEP;

        if (!isBrowser){
            SoundList[Sounds.BBS]        = document.getElementById("Sound_BBS");
            SoundList[Sounds.BBS].sndIdx = Sounds.BBS;
        }
    }

    init();
}

function Create_cPanel(mediaCall, video, share, videoContainer) {
    var Self        = this;
    var isFit       = false;
    var isMax       = false;
    var panelShowed = false;
    var hideTimer   = null;
    var _cPanel     = document.createElement("div");
    var _fade       = new mcService.FadeControl(_cPanel);
    var controls    = {
        cameraSwitch     : document.createElement("span"),
        microphoneSwitch : document.createElement("span"),
        cancelCall       : document.createElement("span"),
        fillFitScreen    : document.createElement("span"),
        minMaxScreen     : document.createElement("span"),
        recordCall       : document.createElement("span"),
    };

    _cPanel.id                   = "cPanel";
    controls.cameraSwitch    .id = "cameraSwitch";
    controls.microphoneSwitch.id = "microphoneSwitch";
    controls.cancelCall      .id = "cancelCall";
    controls.fillFitScreen   .id = "fillFitScreen";
    controls.minMaxScreen    .id = "minMaxScreen";
    controls.recordCall      .id = "recordCall";

    controls.cameraSwitch .style.display = video ? "inline-block" : "none";
    controls.fillFitScreen.style.display = share ? "inline-block" : "none";
    controls.minMaxScreen .style.display = video || share ? "inline-block" : "none";

    // _cPanel.appendChild(controls.recordCall);
    _cPanel.appendChild(controls.cameraSwitch);
    _cPanel.appendChild(controls.microphoneSwitch);
    _cPanel.appendChild(controls.fillFitScreen);
    _cPanel.appendChild(controls.minMaxScreen);
    _cPanel.appendChild(controls.cancelCall);

    // ------------------------

    function showPanel() {
        if (panelShowed){
            clearTimeout(hideTimer);

            hideTimer = setTimeout(function () {
                panelShowed = false;
                hideTimer   = null;

                _fade.out();
            }, 4000);
        } else {
            _fade.in();

            panelShowed = true;
        }
    }

    // ------------------------

    controls.cameraSwitch.addEventListener('click', function(){
        if (mcService.checkRights(mcConst._CMD_.RS.QPrivAllowDisableCameraDuringCalls)){
            if (mediaCall.switchCamera()){
                controls.cameraSwitch.className = "";
            } else {
                controls.cameraSwitch.className = "cameraSwitchOFF";
            }
        }
    });

    controls.microphoneSwitch.addEventListener('click', function(){
        if (mcService.checkRights(mcConst._CMD_.RS.QPrivAllowMuteSoundDuringCalls)){
            if (mediaCall.switchMicrophone()) {
                controls.microphoneSwitch.className = "";
            } else {
                controls.microphoneSwitch.className = "microphoneSwitchOFF";
            }
        }
    });

    controls.fillFitScreen.addEventListener('click', function(){
        if (Self.fitFill) Self.fitFill(isFit, videoContainer);

        controls.fillFitScreen.className = isFit ? "" : "fillFitScreenOFF";

        isFit = !isFit;
    });

    controls.minMaxScreen.addEventListener('click', function () {
        if (Self.minMax) Self.minMax(isMax);

        isMax = !isMax;
    });

    controls.cancelCall.addEventListener('click', function () {
        if (mcService.checkRights(mcConst._CMD_.RS.QPrivAllowHangUpCalls) && Self.closeCall) Self.closeCall();
    });

    controls.recordCall.addEventListener('click', function () {
        if (mcService.checkRights(mcConst._CMD_.RS.QPrivAllowRecordMediaCalls) && Self.record) Self.record();
    });

    // --------------------------------

    if (!mcService.checkRights(mcConst._CMD_.RS.QPrivAllowMuteSoundDuringCalls)) {
        controls.microphoneSwitch.style.opacity = .3;
    }

    if (!mcService.checkRights(mcConst._CMD_.RS.QPrivAllowDisableCameraDuringCalls)) {
        controls.cameraSwitch.style.opacity = .3;
    }

    if (!mcService.checkRights(mcConst._CMD_.RS.QPrivAllowHangUpCalls)) {
        controls.cancelCall.style.opacity = .3;
    }

    if (!mcService.checkRights(mcConst._CMD_.RS.QPrivAllowRecordMediaCalls)) {
        controls.recordCall.style.opacity = .3;
    }

    // --------------------------------

    this.body      = _cPanel;
    this.closeCall = null;
    this.minMax    = null;
    this.fitFill   = null;
    this.record    = null;

    this.init = function (parent) {
        if (parent){
            parent.appendChild(_cPanel);
        }
    };

    this.show = function () {
        _cPanel.className    += "whitePanelBG";
        _cPanel.style.opacity = 1;

        panelShowed = true;

        showPanel();
    };

    this.onmousemove = showPanel;

    this.destructor = function () {
        if (_cPanel){
            _cPanel.innerHTML = "";

            if (_cPanel.parentNode) {
                _cPanel.parentNode.removeChild(_cPanel);
            }

            _cPanel = null;
        }
    };
}
/*
 * MIT License
 *  You may use this code as long as you retain this notice.  Use at your own risk! :)
 *  https://github.com/danschumann/limby-resize
 *  0.0.4
 */
var canvasResize = function (original, canvas, cb) {
    var
        w1 = original.width,
        h1 = original.height,
        w2 = canvas.width,
        h2 = canvas.height,
        img = original.getContext("2d").getImageData(0, 0, w1, h1),
        img2 = canvas.getContext("2d").getImageData(0, 0, w2, h2);

    if (w2 > w1 || h2 > h1) {
        canvas.getContext('2d').drawImage(original, 0, 0, w2, h2);

        if (cb) {
            cb();
        }
    } else {
        var data = img.data;
        // we don't use this much, as working with doubles isn't great
        var _data2 = img2.data;

        // We enforce float type for every entity in the array
        // this prevents weird faded lines when things get rounded off
        var data2 = Array(_data2.length);

        for (var i = 0; i < _data2.length; i++) {
            data2[i] = 0.0;
        }

        // We track alphas, since we need to use alphas to correct colors later on
        var alphas = Array(_data2.length >> 2);
        for (var i = 0; i < _data2.length >> 2; i++) {
            alphas[i] = 1;
        }

        // when resizing down, this will be decimal
        var xScale = w2 / w1;
        var yScale = h2 / h1;

        // For every pixel in the original, we tally it up in the new one
        var nextY = function (y1) {
            for (var x1 = 0; x1 < w1; x1++) {
                var

                    // the original pixel is split between two pixels in the output, we do an extra step
                    extraX = false,
                    extraY = false,

                    // the output pixel
                    targetX = Math.floor(x1 * xScale),
                    targetY = Math.floor(y1 * yScale),

                    // The percentage of this pixel going to the output pixel
                    xFactor = xScale,
                    yFactor = yScale,

                    // The percentage of this pixel going to the right neighbor or bottom neighbor
                    bottomFactor = 0,
                    rightFactor = 0,

                    // positions of pixels in the array
                    offset = (y1 * w1 + x1) * 4,
                    targetOffset = (targetY * w2 + targetX) * 4;

                // Right side goes into another pixel
                if (targetX < Math.floor((x1 + 1) * xScale)) {

                    rightFactor = (((x1 + 1) * xScale) % 1);
                    xFactor -= rightFactor;

                    extraX = true;

                }

                // Bottom side goes into another pixel
                if (targetY < Math.floor((y1 + 1) * yScale)) {

                    bottomFactor = (((y1 + 1) * yScale) % 1);
                    yFactor -= bottomFactor;

                    extraY = true;

                }

                var a;

                a = (data[offset + 3] / 255);

                var alphaOffset = targetOffset / 4;

                if (extraX) {

                    // Since we're not adding the color of invisible pixels,  we multiply by a
                    data2[targetOffset + 4] += data[offset] * rightFactor * yFactor * a;
                    data2[targetOffset + 5] += data[offset + 1] * rightFactor * yFactor * a;
                    data2[targetOffset + 6] += data[offset + 2] * rightFactor * yFactor * a;

                    data2[targetOffset + 7] += data[offset + 3] * rightFactor * yFactor;

                    // if we left out the color of invisible pixels(fully or partly)
                    // the entire average we end up with will no longer be out of 255
                    // so we subtract the percentage from the alpha ( originally 1 )
                    // so that we can reverse this effect by dividing by the amount.
                    // ( if one pixel is black and invisible, and the other is white and visible,
                    // the white pixel will weight itself at 50% because it does not know the other pixel is invisible
                    // so the total(color) for the new pixel would be 128(gray), but it should be all white.
                    // the alpha will be the correct 128, combinging alphas, but we need to preserve the color
                    // of the visible pixels )
                    alphas[alphaOffset + 1] -= (1 - a) * rightFactor * yFactor;
                }

                if (extraY) {
                    data2[targetOffset + w2 * 4] += data[offset] * xFactor * bottomFactor * a;
                    data2[targetOffset + w2 * 4 + 1] += data[offset + 1] * xFactor * bottomFactor * a;
                    data2[targetOffset + w2 * 4 + 2] += data[offset + 2] * xFactor * bottomFactor * a;

                    data2[targetOffset + w2 * 4 + 3] += data[offset + 3] * xFactor * bottomFactor;

                    alphas[alphaOffset + w2] -= (1 - a) * xFactor * bottomFactor;
                }

                if (extraX && extraY) {
                    data2[targetOffset + w2 * 4 + 4] += data[offset] * rightFactor * bottomFactor * a;
                    data2[targetOffset + w2 * 4 + 5] += data[offset + 1] * rightFactor * bottomFactor * a;
                    data2[targetOffset + w2 * 4 + 6] += data[offset + 2] * rightFactor * bottomFactor * a;

                    data2[targetOffset + w2 * 4 + 7] += data[offset + 3] * rightFactor * bottomFactor;

                    alphas[alphaOffset + w2 + 1] -= (1 - a) * rightFactor * bottomFactor;
                }

                data2[targetOffset] += data[offset] * xFactor * yFactor * a;
                data2[targetOffset + 1] += data[offset + 1] * xFactor * yFactor * a;
                data2[targetOffset + 2] += data[offset + 2] * xFactor * yFactor * a;

                data2[targetOffset + 3] += data[offset + 3] * xFactor * yFactor;

                alphas[alphaOffset] -= (1 - a) * xFactor * yFactor;
            }

            if (y1++ < h1)
                setTimeout(nextY.bind(this, y1), 0); // this allows other processes to tick
            else
                done();

        };

        nextY(0);

        var done = function () {

            // fully distribute the color of pixels that are partially full because their neighbor is transparent
            for (var i = 0; i < (_data2.length >> 2); i++) {
                if (alphas[i] && alphas[i] < 1) {
                    data2[(i << 2)] /= alphas[i];     // r
                    data2[(i << 2) + 1] /= alphas[i]; // g
                    data2[(i << 2) + 2] /= alphas[i]; // b
                }
            }

            // re populate the actual imgData
            for (var i = 0; i < data2.length; i++) {
                _data2[i] = Math.round(data2[i]);
            }

            var context = canvas.getContext("2d");

            context.putImageData(img2, 0, 0);
            //deferred.resolve();

            if (cb) {
                cb();
            }
        };
    }
};

// Allow for use with browser or node.js
if ('undefined' !== typeof window) {
    window.canvasResize = canvasResize;
} else {
    module.exports = canvasResize;
}

function mcConnect () {
    "use strict";

    var CLIENT_TO_SERVER = 'client-to-server-cmd';
    var SERVER_TO_CLIENT = 'server-to-client-cmd';

    var CallBackFunctionPull = {};
    var sendStack = [];
    var self      = this;
    var _config   = {
        idError              : 0,
        AjaxInProgress       : false,
        _Timeout             : 20000,
        CRLF                 : "\r\n",
        CR                   : "\r",
        LF                   : "\n"
    };

    self.NoCMD          = false;
    self.CallBackNoCMD  = null;
    self.LeaveCallBacks = false;

    function check(idx, CMD) {
        CallBackFunctionPull[CMD].splice(idx, 1);

        if (CallBackFunctionPull[CMD].length === 0) {
            delete CallBackFunctionPull[CMD];
        }

        return CallBackFunctionPull.hasOwnProperty(CMD) ? CallBackFunctionPull[CMD].length > 0 : false;
    }
    
    function isObject(obj) {
        return Object.prototype.toString.call( obj ) === '[object Object]';
    }

    function requestError(XMLHttpRequest) {
        CallBackFunctionPull = {};
        sendStack = [];

        if (XMLHttpRequest >= 1 || XMLHttpRequest === undefined){
            if (self.OnError) {
                //self.OnError((XMLHttpRequest == '' || !XMLHttpRequest)? "error" : XMLHttpRequest);
                self.OnError("error");
            }

            if (mcConst.socketIO){
                mcConst.socketIO.disconnect();

                // throw new Error("Network error");
            }
        }
    }

    function __SendData(_data){
        mcConst.socketIO.emit(CLIENT_TO_SERVER, _data);
    }

    function RequestSuccess(msg){
        var CR_IDX = msg.length ? msg.indexOf(_config.CR) : -1;

        // console.log(msg);

        if (CR_IDX !== -1) {
            var cmd  = msg.slice(0, CR_IDX);
            var data = msg.slice(CR_IDX + 1, msg.length + 1);

            if (self.NoCMD) {
                self.CallBackNoCMD.apply(msg);
            } else {
                self.RunCallBackFunctionByCMD(cmd, data);
            }
        } else {
            var cmd  = "ping";

            if (self.NoCMD) {
                self.CallBackNoCMD.apply(msg);
            } else {
                self.RunCallBackFunctionByCMD(cmd, msg);
            }
        }
    }

    /**
     * @return {number}
     */
    self.SendDataToServer = function(_data){
        var sendingData = '';
        var prepareData = {};

        if ((!_data) || (_data.length === 0)) {
            if (console && console.warn) {
                console.warn('SendDataToServer: _data is empty or undefined');
            }

            return 1;
        } else {
            switch (typeof _data){
                case "string":
                    sendingData = _data;
                break;

                case "object":
                     prepareData = mcService.Marge({
                        CMD        : '',
                        Data       : '',
                        OnReceive  : null
                    }, _data || {});

                    if (prepareData.Data === ''){
                        console.error('SendDataToServer: "Data" is undefined');

                        return 2;
                    }

                    if (isObject(prepareData.Data)){
                        prepareData.Data = JSON.stringify(prepareData.Data);
                    }

                    sendingData = prepareData.CMD + _config.CR + prepareData.Data;

                    if (prepareData.CMD !== '' && prepareData.OnReceive){
                        self.SetCallBackFunctionByCMD(prepareData.CMD, prepareData.OnReceive);
                    }
                break;
            }

            __SendData(sendingData, prepareData.immediately);
        }
    };

    //=====================================================================
    self.OnError        = undefined;

    //=====================================================================
    self.SetCallBackFunctionByCMD = function(CMD, callback){
        if (callback){
            if (!CallBackFunctionPull.hasOwnProperty(CMD)) {
                CallBackFunctionPull[CMD] = [];
            }

            CallBackFunctionPull[CMD].push(callback);
        }
    };

    //=====================================================================
    self.ClearAllCallBackFunctionByCMD = function(){
        for (var i in CallBackFunctionPull){
            if (CallBackFunctionPull.hasOwnProperty(i)) {
                delete CallBackFunctionPull[i];
            }
        }
    };

    //=====================================================================
    self.RunCallBackFunctionByCMD = function(CMD, MSG){
        var next = CallBackFunctionPull.hasOwnProperty(CMD) ? CallBackFunctionPull[CMD].length > 0 : false;
        var idx  = 0;

        if (!next && CMD != 'OK') {
            console.warn("CMD: \"" + CMD + "\" not have request func");

            requestError();
        }

        while (next){
            if (CallBackFunctionPull[CMD] && CallBackFunctionPull[CMD][idx]){
                if (isObject(CallBackFunctionPull[CMD][idx])) {
                    CallBackFunctionPull[CMD][idx].cb.apply(MSG);

                    if (CallBackFunctionPull[CMD][idx].one){
                        next = check(idx, CMD);

                        continue;
                    }
                } else {
                    CallBackFunctionPull[CMD][idx].apply(MSG);
                }

                if (self.LeaveCallBacks){
                    idx++;
                } else {
                    next = check(idx, CMD);
                }
            } else {
                next = false;
            }
        }
    };

    mcConst.initWS = function (socketIO) {
        socketIO.on(SERVER_TO_CLIENT, RequestSuccess);
        socketIO.on('error',          requestError);
        socketIO.on('connect_failed', requestError);
        socketIO.on('disconnected',   requestError);
        socketIO.on('reconnecting',   requestError);
    }
}

/**
 * Created by Gifer on 10.07.2015.
 */

function mcFileUploading($rootScope){
    "use strict";

    var uploader           = null;
    var errorCallback      = null;
    var successCallback    = null;
    var uploaderOptions    = {};
    var inputFileName      = "__fileSender__";
    var abortUploadingID   = null;
    var abortPrepUploading = null;
    var abortUploadingCB   = null;
    var abortNow           = false;
    var whereWeWork        = "";

    function parseFile(file, callback) {
        var fileSize  = file.size;
        var chunkSize = 512 * 1024; // bytes
        var offset    = 0;
        var chunkReaderBlock = null;
        var onePercent     = file.size / 100;
        var currentPercent = 0;

        var readEventHandler = function (evt) {
            if (!abortNow){
                if (evt.target.error == null) {
                    offset += evt.target.result.byteLength;
                    currentPercent = (offset / onePercent).toFixed();

                    callback(null, evt.target.result, currentPercent); // callback for handling read chunk
                } else {
                    callback(evt.target.error);

                    return;
                }

                if (offset >= fileSize) {
                    callback(null, null, 100);

                    return;
                }

                chunkReaderBlock(fileReader, offset, chunkSize, file);
            }
        };

        chunkReaderBlock = function (_reader, _offset, length, _file) {
            var reader = _reader || new FileReader();
            var blob   = _file.slice(_offset, length + _offset);

            if (!_reader) {
                if (uploaderOptions.onBeforeFileAdd) {
                    uploaderOptions.onBeforeFileAdd(file);
                }

                if (!abortPrepUploading){
                    abortPrepUploading = function () {
                        reader.abort();
                        reader.onload = null;

                        abortPrepUploading = null;
                    }
                }

                reader.onload = readEventHandler;
            }

            reader.readAsArrayBuffer(blob);

            return reader;
        };

        var fileReader = chunkReaderBlock(null, offset, chunkSize, file);
    }

    function _startUpload (info, file) {
        if (!info.Present) { // file not exist - uploading
            uploader.define("upload", mcService.getLocalHostPath($rootScope.isWebClient) + "/uploading/?sid=" + mcConst.SessionID + "&hash=" + info.Hash + whereWeWork);
            uploader.addFile(file);
        } else {
            if (successCallback) {
                successCallback.apply(uploaderOptions, [info, file]);

                if (uploaderOptions.dropFile && uploaderOptions.dropFile.files && uploaderOptions.fileIdx < uploaderOptions.dropFile.files.length){
                    uploaderOptions.fileIdx ++;

                    _goUploadFile.apply(uploaderOptions.dropFile, [uploaderOptions.fileIdx]);
                }
            } else {
                console.trace('No set success callback on file uploading');
            }
        }
    }

    function startUploadAfterCheck(info, file) {
        uploaderOptions.Hash     = info.Hash; // SHA1 хэш файла
        uploaderOptions.Width    = $rootScope.thumbsSize.x; // произвольная ширина уменьшеной копии изображения
        uploaderOptions.Height   = $rootScope.thumbsSize.y; // произвольная высота уменьшенной копии изображения
        uploaderOptions.FileName = info.FileName; // локальное название файла

        if (file.type === "image/png" || file.type === "image/jpeg" || file.type === "image/gif"){
            var tempImg = document.createElement("img");
            tempImg.src = window.URL.createObjectURL(file);

            tempImg.onload = function () {
                uploaderOptions.resolution = tempImg.width + "x" + tempImg.height; // локальное название файла

                tempImg = null;

                _startUpload(info, file);
            };

            tempImg.onerror = function (err) {
                console.warn(err);

                errorCallback && errorCallback(err);

                errorCallback = null;
            };
        } else {
            _startUpload(info, file);
        }
    }

    function electronUploader() {
        $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
            mcConst._CMD_.ce_file_upload_start,
            uploaderOptions.filePath,
            {
                Where    : uploaderOptions.Where,
                ID       : uploaderOptions.ID,
                Type     : uploaderOptions.Type,
                chatType : uploaderOptions.chatType
            }
        ]);

        abortUploadingCB = function () {
            abortUploadingCB = null;

            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_file_upload_abort
            ]);
        };
        
        abortPrepUploading = function () {
            abortPrepUploading = null;

            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_file_upload_prepare_abort
            ]);
        };
    }

    function checkFileExist(params, cb) {
        $rootScope.SendCMDToServer(
            [ mcConst._CMD_.cs_is_file_exists, mcConst.SessionID ]
            .concat(params || [])
            .concat(cb)
        );
    }

    function _goUploadClipboardImage() {
        var calcSha1   = mcService.SHA1();
        var Hash       = "";
        var fileReader = new FileReader();

        fileReader.onloadend = function() {
            var screenImg = this.result;
            var imageInfo = uploaderOptions.clipboardImage;

            calcSha1.append(screenImg);

            if (!imageInfo.name || (imageInfo.name && imageInfo.name.indexOf("image.") === 0 && !imageInfo.path) ){
                try{
                    imageInfo["name"] = "Screenshot (" + mcConst.UserInfo.Nick + ") " + mcService.formatDate(new Date(), "dd-mm-yyyy hh-nn-ss") + ".png";
                } catch (e) {}
            }

            Hash = calcSha1.end();

            checkFileExist([
                Hash,                         // SHA1 хэш файла
                // mcConst._CMD_.msgType.IMAGE,  // тип файла. 2 - изображение, 4 - обычный файл
                mcConst._CMD_.msgType.IMAGE_NEW,  // тип файла. 2 - изображение, 4 - обычный файл
                imageInfo.name,               // локальное название файла
                uploaderOptions.Where,
                uploaderOptions.ID,
                mcService.fileTimeStamp(),
                imageInfo.size
            ], function (data) {
                startUploadAfterCheck(data, imageInfo);
            });
        };

        fileReader.readAsArrayBuffer(uploaderOptions.clipboardImage);
    }

    function _goUploadFile(id) {
        var fileInput = this;
        var calcSha1  = mcService.SHA1();
        var fileIdx   = mcService.isNumber(id) ? id : 0;
        var file      = fileInput.files[fileIdx];
        var MsgType   = uploaderOptions.Type;

        uploaderOptions.dropFile = { files: mcService.Marge({ length : fileInput.files.length }, fileInput.files) };
        uploaderOptions.fileIdx  = fileIdx;

        if (file){
            uploaderOptions.fileSize = file.size;
            uploaderOptions.lastModifiedDate = file.lastModified || file.lastModifiedDate;

            if (!mcConst.UserInfo.MaxUploadFileSize || mcConst.UserInfo.MaxUploadFileSize > uploaderOptions.fileSize){
                setTimeout(function () {
                    parseFile(file, function (err, data, percent) {
                        if (err) {
                            console.error("Read error: " + err);
                        } else {
                            if (data) {
                                if (uploaderOptions.onPrepareProgress){
                                    uploaderOptions.onPrepareProgress(percent);
                                }

                                calcSha1.append(data);
                            } else {
                                var Hash = calcSha1.end();

                                if (uploaderOptions.onBeforeUpload && percent === 100){
                                    uploaderOptions.onBeforeUpload(Hash);
                                }

                                if (fileInput.value) {
                                    fileInput.value = "";
                                }

                                checkFileExist([
                                    Hash    ,                 // SHA1 хэш файла
                                    MsgType ,                 // тип файла. 2 - изображение, 4 - обычный файл
                                    // uploaderOptions.Type === mcConst._CMD_.msgType.IMAGE
                                    uploaderOptions.Type === mcConst._CMD_.msgType.IMAGE_NEW
                                        ? file.name           // локальное название файла
                                        : file.path || file.name,
                                    uploaderOptions.Where   , // куда вставлять файл (priv, conf, bbs, broadcast, forum, kanban)
                                    uploaderOptions.ID      , // число-идентификатор, для кого отправлять файл:
                                                              // 1, private    - UIN
                                                              // 2, conference - UID
                                                              // 3, forum      - ID топика
                                                              // 4, kanban     - ID таска
                                                              // 5, bbs        - -1
                                                              // 6, broadcast  - -1
                                    mcService.fileTimeStamp(file.lastModified || file.lastModifiedDate),
                                    uploaderOptions.fileSize
                                ], function (data) {
                                    startUploadAfterCheck(data, file);
                                });
                            }
                        }
                    });
                }, 100);
            } else {
                if (uploader) {
                    uploader.destructor();
                }

                if (errorCallback){
                    errorCallback();

                    $rootScope.$broadcast('ErrorMsg', [255, mcService.Error(255), [mcConst.UserInfo.MaxUploadFileSize], function(){ // "255": "администратор сервера ограничил максимальный размер файла для заливки в %s MB",
                        $rootScope.$broadcast('hide' + mcConst.lockInterface, ["all"]); // unlock
                    }]);
                }

                errorCallback   = null;
                successCallback = null;
                uploaderOptions = {};
            }
        }
    }

    function fileFromDialog() {
        var element  = document.getElementById(inputFileName);

        if (!element){
            element = document.createElement('div');

            element.id        = inputFileName;
            element.className = 'hideFileInput';

            document.body.appendChild(element);
        }

        element.innerHTML = '<input type="file">';

        var fileInput = element.firstChild;

        if (fileInput.value && mcService.isIE()) {
            fileInput.parentNode.replaceChild(
                fileInput.cloneNode(true),
                fileInput
            );

            fileInput = document.getElementById(inputFileName).firstChild;
        }

        switch (uploaderOptions.Type) {
            case mcConst._CMD_.msgType.IMAGE_NEW:
            case mcConst._CMD_.msgType.IMAGE:
                fileInput.accept = "image/png, image/gif, image/jpeg, image/jpg";
            break;

            case mcConst._CMD_.msgType.VIDEO:
                fileInput.accept = "video/mp4, video/x-m4v, .mkv, video/mpeg, video/webm, video/*";
            break;

            default:
                fileInput.accept = "";
        }

        if (fileInput.fileEvent){
            fileInput.removeEventListener('change', fileInput.fileEvent);
        }

        fileInput.fileEvent = _goUploadFile;
        fileInput.multiple  = true;

        fileInput.addEventListener('change', fileInput.fileEvent);

        fileInput.click();
    }

    function uploadFile(_errorCallback, _successCallback, _uploaderOptions){
        errorCallback   = _errorCallback;
        successCallback = _successCallback;
        uploaderOptions = _uploaderOptions || {};

        // -- clipboard image --
        if (uploaderOptions.clipboardImage) {
            _goUploadClipboardImage();
        } else

        // -- drop file into chat text --
        if (uploaderOptions.filePath && uploaderOptions.dropFile){
            _goUploadFile.apply(uploaderOptions.dropFile);
        } else

        // -- clipboard file, only for electron app --
        if (uploaderOptions.filePath){
            electronUploader();
        } else
        
        // -- file from dialog window --
        {
            fileFromDialog();
        }
    }

    // ========================================================

    var _msg = window._messages_.mcFileUploader = {
        abortUploading          : 'abortUploading',
        abortPreparingUploading : 'abortPreparingUploading',
        uploadFile              : 'uploadFile'
    };

    $rootScope.$on(_msg.abortUploading, function () {
        abortNow = true;

        if (abortPrepUploading) {
            abortPrepUploading();
        }

        if (uploader && abortUploadingID) {
            uploader.stopUpload(abortUploadingID);
            uploader.destructor();
        }

        if (abortUploadingCB){
            abortUploadingCB();
        }

        uploaderOptions = {};
    });

    $rootScope.$on(_msg.abortPreparingUploading, function () {
        if (abortPrepUploading) {
            abortPrepUploading();
        }

        if (uploader){
            uploader.destructor();
        }
    });

    $rootScope.$on(_msg.uploadFile, function(e,args){
        uploader = $$("uploadAPI");

        if (uploader){
            uploader.destructor();
        }

        abortUploadingID = null;
        abortPrepUploading = null;
        abortUploadingCB = null;
        abortNow = false;

        whereWeWork = $rootScope.hasOwnProperty("GetChatType") ? "&where=" + $rootScope.GetChatType().toLowerCase() : "";

        var opt = args[2] || {};

        if (!opt.uploadProgress && !$$("uploadProgress")){
            webix.ui({ id: "uploadProgress", view: "list", scroll: false, padding: 0, borderless: true, hidden: true, select: false});
        }

        uploader = webix.ui({
            id     : "uploadAPI",
            view   : "uploader",
            multiple: false,
            link   : opt.uploadProgress || "uploadProgress",
            apiOnly: true,
            on     : {
                onAfterFileAdd: function (file) {
                    abortUploadingID = file.id;

                    if (uploaderOptions.onAfterFileAdd){
                        uploaderOptions.onAfterFileAdd(file);
                    }
                },
                onUploadComplete:function(){
                    if (successCallback) {
                        successCallback.apply(uploaderOptions, []);
                    } else {
                        console.trace('No set success callback on file uploading');
                    }

                    if (uploaderOptions.dropFile && uploaderOptions.dropFile.files && uploaderOptions.fileIdx < uploaderOptions.dropFile.files.length){
                        uploaderOptions.fileIdx ++;

                        _goUploadFile.apply(uploaderOptions.dropFile, [uploaderOptions.fileIdx]);
                    } else {
                        errorCallback = null;
                        successCallback = null;
                    }
                },
                onFileUploadError:function(){
                    console.warn("Error during file uploading");

                    if (errorCallback) {
                        errorCallback.apply(null, arguments);
                    }

                    errorCallback = null;
                    successCallback = null;
                }
            }
        });
 
        uploader.$updateProgress = function(id, percent) {
            var item = this.files.getItem(id);
            item.percent = Math.round(percent);

            this.files.updateItem(id);
        };

       uploadFile.apply(null, args);
    });
}
/*!
 * Bowser - a browser detector
 * https://github.com/ded/bowser
 * MIT License | (c) Dustin Diaz 2014
 */

var browserDetect = (function () {
    var t = true;

    function detect(ua) {

        function getFirstMatch(regex) {
            var match = ua.match(regex);
            return (match && match.length > 1 && match[1]) || '';
        }

        function getSecondMatch(regex) {
            var match = ua.match(regex);
            return (match && match.length > 1 && match[2]) || '';
        }

        var iosdevice = getFirstMatch(/(ipod|iphone|ipad)/i).toLowerCase()
            , likeAndroid = /like android/i.test(ua)
            , android = !likeAndroid && /android/i.test(ua)
            , edgeVersion = getFirstMatch(/edge\/(\d+(\.\d+)?)/i)
            , versionIdentifier = getFirstMatch(/version\/(\d+(\.\d+)?)/i)
            , tablet = /tablet/i.test(ua)
            , mobile = !tablet && /[^-]mobi/i.test(ua)
            , result;

        if (/opera|opr/i.test(ua)) {
            result = {
                name: 'Opera'
                , opera: t
                , version: versionIdentifier || getFirstMatch(/(?:opera|opr)[\s\/](\d+(\.\d+)?)/i)
            }
        }
        else if (/yabrowser/i.test(ua)) {
            result = {
                name: 'Yandex Browser'
                , yandexbrowser: t
                , version: versionIdentifier || getFirstMatch(/(?:yabrowser)[\s\/](\d+(\.\d+)?)/i)
            }
        }
        else if (/(windows phone|lumia)/i.test(ua)) {

            result = {
                name: 'Windows Phone'
                , windowsphone: t
            };
            if (edgeVersion) {
                result.msedge = t;
                result.version = edgeVersion
            }
            else {
                result.msie = t;
                result.version = getFirstMatch(/iemobile\/(\d+(\.\d+)?)/i)
            }
        }
        else if (/msie|trident/i.test(ua)) {
            result = {
                name: 'Internet Explorer'
                , msie: t
                , version: getFirstMatch(/(?:msie |rv:)(\d+(\.\d+)?)/i)
            }
        }
        else if (/chrome.+? edge/i.test(ua)) {
            result = {
                name: 'Microsoft Edge'
                , msedge: t
                , version: edgeVersion
            }
        }
        else if (/chrome|crios|crmo/i.test(ua)) {
            result = {
                name: 'Chrome'
                , chrome: t
                , version: getFirstMatch(/(?:chrome|crios|crmo)\/(\d+(\.\d+)?)/i)
            }
        }
        else if (iosdevice) {
            result = {
                name : iosdevice == 'iphone' ? 'iPhone' : iosdevice == 'ipad' ? 'iPad' : 'iPod'
            };
            // WTF: version is not part of user agent in web apps
            if (versionIdentifier) {
                result.version = versionIdentifier
            }
        }
        else if (/sailfish/i.test(ua)) {
            result = {
                name: 'Sailfish'
                , sailfish: t
                , version: getFirstMatch(/sailfish\s?browser\/(\d+(\.\d+)?)/i)
            }
        }
        else if (/seamonkey\//i.test(ua)) {
            result = {
                name: 'SeaMonkey'
                , seamonkey: t
                , version: getFirstMatch(/seamonkey\/(\d+(\.\d+)?)/i)
            }
        }
        else if (/firefox|iceweasel/i.test(ua)) {
            result = {
                name: 'Firefox'
                , firefox: t
                , version: getFirstMatch(/(?:firefox|iceweasel)[ \/](\d+(\.\d+)?)/i)
            };
            if (/\((mobile|tablet);[^\)]*rv:[\d\.]+\)/i.test(ua)) {
                result.firefoxos = t
            }
        }
        else if (/silk/i.test(ua)) {
            result =  {
                name: 'Amazon Silk'
                , silk: t
                , version : getFirstMatch(/silk\/(\d+(\.\d+)?)/i)
            }
        }
        else if (android) {
            result = {
                name: 'Android'
                , version: versionIdentifier
            }
        }
        else if (/phantom/i.test(ua)) {
            result = {
                name: 'PhantomJS'
                , phantom: t
                , version: getFirstMatch(/phantomjs\/(\d+(\.\d+)?)/i)
            }
        }
        else if (/blackberry|\bbb\d+/i.test(ua) || /rim\stablet/i.test(ua)) {
            result = {
                name: 'BlackBerry'
                , blackberry: t
                , version: versionIdentifier || getFirstMatch(/blackberry[\d]+\/(\d+(\.\d+)?)/i)
            }
        }
        else if (/(web|hpw)os/i.test(ua)) {
            result = {
                name: 'WebOS'
                , webos: t
                , version: versionIdentifier || getFirstMatch(/w(?:eb)?osbrowser\/(\d+(\.\d+)?)/i)
            };
            /touchpad\//i.test(ua) && (result.touchpad = t)
        }
        else if (/bada/i.test(ua)) {
            result = {
                name: 'Bada'
                , bada: t
                , version: getFirstMatch(/dolfin\/(\d+(\.\d+)?)/i)
            };
        }
        else if (/tizen/i.test(ua)) {
            result = {
                name: 'Tizen'
                , tizen: t
                , version: getFirstMatch(/(?:tizen\s?)?browser\/(\d+(\.\d+)?)/i) || versionIdentifier
            };
        }
        else if (/safari/i.test(ua)) {
            result = {
                name: 'Safari'
                , safari: t
                , version: versionIdentifier
            }
        }
        else {
            result = {
                name: getFirstMatch(/^(.*)\/(.*) /),
                version: getSecondMatch(/^(.*)\/(.*) /)
            };
        }

        // set webkit or gecko flag for browsers based on these engines
        if (!result.msedge && /(apple)?webkit/i.test(ua)) {
            result.name = result.name || "Webkit";
            result.webkit = t;
            if (!result.version && versionIdentifier) {
                result.version = versionIdentifier
            }
        } else if (!result.opera && /gecko\//i.test(ua)) {
            result.name = result.name || "Gecko";
            result.gecko = t;
            result.version = result.version || getFirstMatch(/gecko\/(\d+(\.\d+)?)/i)
        }

        // set OS flags for platforms that have multiple browsers
        if (!result.msedge && (android || result.silk)) {
            result.android = t
        } else if (iosdevice) {
            result[iosdevice] = t;
            result.ios = t
        }

        // OS version extraction
        var osVersion = '';
        if (result.windowsphone) {
            osVersion = getFirstMatch(/(windows phone|lumia) (?:os)?\s?(\d+(\.\d+)*)/i);
        } else if (iosdevice) {
            osVersion = getFirstMatch(/os (\d+([_\s]\d+)*) like mac os x/i);
            osVersion = osVersion.replace(/[_\s]/g, '.');
        } else if (android) {
            osVersion = getFirstMatch(/android[ \/-](\d+(\.\d+)*)/i);
        } else if (result.webos) {
            osVersion = getFirstMatch(/(?:web|hpw)os\/(\d+(\.\d+)*)/i);
        } else if (result.blackberry) {
            osVersion = getFirstMatch(/rim\stablet\sos\s(\d+(\.\d+)*)/i);
        } else if (result.bada) {
            osVersion = getFirstMatch(/bada\/(\d+(\.\d+)*)/i);
        } else if (result.tizen) {
            osVersion = getFirstMatch(/tizen[\/\s](\d+(\.\d+)*)/i);
        }
        if (osVersion) {
            result.osversion = osVersion;
        }

        // device type extraction
        var osMajorVersion = osVersion.split('.')[0];
        if (tablet || iosdevice == 'ipad' || (android && (osMajorVersion == 3 || (osMajorVersion == 4 && !mobile))) || result.silk) {
            result.tablet = t
        } else if (mobile || iosdevice == 'iphone' || iosdevice == 'ipod' || android || result.blackberry || result.webos || result.bada || result.windowsphone) {
            result.mobile = t
        }

        // Graded Browser Support
        // http://developer.yahoo.com/yui/articles/gbs
        if (result.msedge ||
            (result.msie && result.version >= 10) ||
            (result.yandexbrowser && result.version >= 15) ||
            (result.chrome && result.version >= 20) ||
            (result.firefox && result.version >= 20.0) ||
            (result.safari && result.version >= 6) ||
            (result.opera && result.version >= 10.0) ||
            (result.ios && result.osversion && result.osversion.split(".")[0] >= 6) ||
            (result.blackberry && result.version >= 10.1)
            ) {
            result.a = t;
        }
        else if ((result.msie && result.version < 10) ||
            (result.chrome && result.version < 20) ||
            (result.firefox && result.version < 20.0) ||
            (result.safari && result.version < 6) ||
            (result.opera && result.version < 10.0) ||
            (result.ios && result.osversion && result.osversion.split(".")[0] < 6)
            ) {
            result.c = t
        } else result.x = t;

        return result
    }

    var _browser = detect(typeof navigator !== 'undefined' ? navigator.userAgent : '');

    _browser.test = function (browserList) {
        for (var i = 0; i < browserList.length; ++i) {
            var browserItem = browserList[i];
            if (typeof browserItem=== 'string') {
                if (browserItem in _browser) {
                    return true;
                }
            }
        }
        return false;
    };

    /*
     * Set our detect method to the main browser object so we can
     * reuse it to test other user agents.
     * This is needed to implement future tests.
     */
    _browser._detect = detect;

    return _browser
})();

/*
 * TouchScroll - using dom overflow:scroll
 * by kmturley
 */

/*globals window, document */

(function() {
    var lastTime = 0;
    var vendors = ['ms', 'moz', 'webkit', 'o'];
    for(var x = 0; x < vendors.length && !window.requestAnimationFrame; ++x) {
        window.requestAnimationFrame = window[vendors[x]+'RequestAnimationFrame'];
        window.cancelAnimationFrame = window[vendors[x]+'CancelAnimationFrame']
            || window[vendors[x]+'CancelRequestAnimationFrame'];
    }

    if (!window.requestAnimationFrame)
        window.requestAnimationFrame = function(callback, element) {
            var currTime = new Date().getTime();
            var timeToCall = Math.max(0, 16 - (currTime - lastTime));
            var id = window.setTimeout(function() { callback(currTime + timeToCall); },
                timeToCall);
            lastTime = currTime + timeToCall;
            return id;
        };

    if (!window.cancelAnimationFrame)
        window.cancelAnimationFrame = function(id) {
            clearTimeout(id);
        };
}());

var TouchScroll = function () {
    'use strict';

    var module = {
        axis: 'x',
        drag: false,
        zoom: 1,
        time: 0.04,
        isIE: window.navigator.userAgent.toLowerCase().indexOf('msie') > -1,
        isFirefox: window.navigator.userAgent.toLowerCase().indexOf('firefox') > -1,
        /**
         * @method init
         */
        init: function (options) {
            var me = this;
            this.options = options;
            
            // find target element or fall back to body
            if (options && options.id) {
                this.el = document.getElementById(options.id);
            }
            if (!this.el) {
                if (this.isIE || this.isFirefox) {
                    this.el = document.documentElement;
                } else {
                    this.el = document.body;
                }
            }

            // if draggable option is enabled add events
            if (options.draggable === true) {
                if (this.isIE) {
                    document.ondragstart = function () { return false; };
                }
                if (this.isIE || this.isFirefox) {
                    this.body = document.documentElement;
                } else {
                    this.body = document.body;
                }
                this.addEvent('touchstart', this.el, function (e) { me.onMouseDown(e); });
                this.addEvent('touchmove', this.el, function (e) { me.onMouseMove(e); });
                this.addEvent('touchend', this.body, function (e) { me.onMouseUp(e); });
            }
            
            // if zoom option exists add mouse wheel functionality to element
            if (options && options.zoom) {
                this.elzoom = document.getElementById(options.zoom);
                if (this.isFirefox) {
                    this.addEvent('DOMMouseScroll', this.el, function (e) { me.onMouseWheel(e); });
                } else {
                    this.addEvent('mousewheel', this.el, function (e) { me.onMouseWheel(e); });
                }
            }
            
            // if scroll options exist add events
            if (options && options.prev) {
                this.prev = document.getElementById(options.prev);
                this.addEvent('touchstart', this.prev, function (e) {
                    me.onMouseDown(e);
                });
                this.addEvent('touchend', this.prev, function (e) {
                    me.diffx = options.distance ? (-options.distance / 11) : -11;
                    me.onMouseUp(e);
                });
            }
            if (options && options.next) {
                this.next = document.getElementById(options.next);
                this.addEvent('touchstart', this.next, function (e) {
                    me.onMouseDown(e);
                });
                this.addEvent('touchend', this.next, function (e) {
                    me.diffx = options.distance ? (options.distance / 11) : 11;
                    me.onMouseUp(e);
                });
            }
        },
        /**
         * @method addEvent
         */
        addEvent: function (name, el, func) {
            if (el.addEventListener) {
                el.addEventListener(name, func, false);
            } else if (el.attachEvent) {
                el.attachEvent('on' + name, func);
            } else {
                el[name] = func;
            }
        },
        /**
         * @method cancelEvent
         */
        cancelEvent: function (e) {
            if (!e) { e = window.event; }
            if (e.target && e.target.nodeName === 'IMG') {
                e.preventDefault();
            } else if (e.srcElement && e.srcElement.nodeName === 'IMG') {
                e.returnValue = false;
            }
        },
        /**
         * @method onMouseDown
         */
        onMouseDown: function (e) {
            if (this.drag === false || this.options.wait === false) {
                var touched = event.touches[0] ? event.touches[0] : event;

                this.drag = true;
                this.cancelEvent(e);
                this.startx = touched.clientX + this.el.scrollLeft;
                this.starty = touched.clientY + this.el.scrollTop;
                this.diffx = 0;
                this.diffy = 0;
            }
        },
        /**
         * @method onMouseMove
         */
        onMouseMove: function (e) {
            if (this.drag === true) {
                var touched = event.touches[0] ? event.touches[0] : event;

                this.cancelEvent(e);
                this.diffx = (this.startx - (touched.clientX + this.el.scrollLeft));
                this.diffy = (this.starty - (touched.clientY + this.el.scrollTop));
                //this.el.scrollLeft += this.diffx;
                this.el.scrollTop += this.diffy;
            }
        },
        /**
         * @method onMouseMove
         */
        onMouseUp: function (e) {
            if (this.drag === true) {
                if (!this.options.wait) {
                    this.drag = null;
                }
                this.cancelEvent(e);
                var me = this,
                    start = 1,
                    animate = function () {
                        var step = Math.sin(start);
                        if (step <= 0) {
                            me.diffx = 0;
                            me.diffy = 0;
                            window.cancelAnimationFrame(animate);
                            me.drag = false;
                        } else {
                            me.el.scrollLeft += me.diffx * step;
                            me.el.scrollTop += me.diffy * step;
                            start -= me.time;
                            window.requestAnimationFrame(animate);
                        }
                    };
                animate();
            }
        },
        /**
         * @method onMouseMove
         */
        onMouseWheel: function (e) {
            this.cancelEvent(e);
            if (e.detail) {
                this.zoom -= e.detail;
            } else {
                this.zoom += (e.wheelDelta / 1200);
            }
            if (this.zoom < 1) {
                this.zoom = 1;
            } else if (this.zoom > 10) {
                this.zoom = 10;
            }
            /*
            this.elzoom.style.OTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')';
            this.elzoom.style.MozTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')';
            this.elzoom.style.msTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')';
            this.elzoom.style.WebkitTransform = 'scale(' + this.zoom + ', ' + this.zoom + ')';
            this.elzoom.style.transform = 'scale(' + this.zoom + ', ' + this.zoom + ')';
            */
            this.elzoom.style.zoom = this.zoom * 100 + '%';
            //this.el.scrollLeft += e.wheelDelta / 10;
            //this.el.scrollTop += e.wheelDelta / 8;
        }
    };
    return module;
};
/*
 * Purl (A JavaScript URL parser) v2.3.1
 * Developed and maintanined by Mark Perkins, mark@allmarkedup.com
 * Source repository: https://github.com/allmarkedup/jQuery-URL-Parser
 * Licensed under an MIT-style license. See https://github.com/allmarkedup/jQuery-URL-Parser/blob/master/LICENSE for details.
 */

;(function(factory) {
    if (typeof exports === 'object') {
        exports.purl = factory();
        return;
    }

    if (typeof define === 'function' && define.amd) {
        define(factory);
    } else {
        window.purl = factory();
    }
})(function() {

    var tag2attr = {
            a       : 'href',
            img     : 'src',
            form    : 'action',
            base    : 'href',
            script  : 'src',
            iframe  : 'src',
            link    : 'href',
            embed   : 'src',
            object  : 'data'
        },

        key = ['source', 'protocol', 'authority', 'userInfo', 'user', 'password', 'host', 'port', 'relative', 'path', 'directory', 'file', 'query', 'fragment'], // keys available to query

        aliases = { 'anchor' : 'fragment' }, // aliases for backwards compatability

        parser = {
            strict : /^(?:([^:\/?#]+):)?(?:\/\/((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?))?((((?:[^?#\/]*\/)*)([^?#]*))(?:\?([^#]*))?(?:#(.*))?)/,  //less intuitive, more accurate to the specs
            loose :  /^(?:(?![^:@]+:[^:@\/]*@)([^:\/?#.]+):)?(?:\/\/)?((?:(([^:@]*):?([^:@]*))?@)?([^:\/?#]*)(?::(\d*))?)(((\/(?:[^?#](?![^?#\/]*\.[^?#\/.]+(?:[?#]|$)))*\/?)?([^?#\/]*))(?:\?([^#]*))?(?:#(.*))?)/ // more intuitive, fails on relative paths and deviates from specs
        },

        isint = /^[0-9]+$/;

    function parseUri( url, strictMode ) {
        var str = decodeURI( url ),
        res   = parser[ strictMode || false ? 'strict' : 'loose' ].exec( str ),
        uri = { attr : {}, param : {}, seg : {} },
        i   = 14;

        while ( i-- ) {
            uri.attr[ key[i] ] = res[i] || '';
        }

        // build query and fragment parameters
        uri.param['query'] = parseString(uri.attr['query']);
        uri.param['fragment'] = parseString(uri.attr['fragment']);

        // split path and fragement into segments
        uri.seg['path'] = uri.attr.path.replace(/^\/+|\/+$/g,'').split('/');
        uri.seg['fragment'] = uri.attr.fragment.replace(/^\/+|\/+$/g,'').split('/');

        // compile a 'base' domain attribute
        uri.attr['base'] = uri.attr.host ? (uri.attr.protocol ?  uri.attr.protocol+'://'+uri.attr.host : uri.attr.host) + (uri.attr.port ? ':'+uri.attr.port : '') : '';

        return uri;
    }

    function getAttrName( elm ) {
        var tn = elm.tagName;
        if ( typeof tn !== 'undefined' ) return tag2attr[tn.toLowerCase()];
        return tn;
    }

    function promote(parent, key) {
        if (parent[key].length === 0) return parent[key] = {};
        var t = {};
        for (var i in parent[key]) t[i] = parent[key][i];
        parent[key] = t;
        return t;
    }

    function parse(parts, parent, key, val) {
        var part = parts.shift();
        if (!part) {
            if (isArray(parent[key])) {
                parent[key].push(val);
            } else if ('object' == typeof parent[key]) {
                parent[key] = val;
            } else if ('undefined' == typeof parent[key]) {
                parent[key] = val;
            } else {
                parent[key] = [parent[key], val];
            }
        } else {
            var obj = parent[key] = parent[key] || [];
            if (']' == part) {
                if (isArray(obj)) {
                    if ('' !== val) obj.push(val);
                } else if ('object' == typeof obj) {
                    obj[keys(obj).length] = val;
                } else {
                    obj = parent[key] = [parent[key], val];
                }
            } else if (~part.indexOf(']')) {
                part = part.substr(0, part.length - 1);
                if (!isint.test(part) && isArray(obj)) obj = promote(parent, key);
                parse(parts, obj, part, val);
                // key
            } else {
                if (!isint.test(part) && isArray(obj)) obj = promote(parent, key);
                parse(parts, obj, part, val);
            }
        }
    }

    function merge(parent, key, val) {
        if (~key.indexOf(']')) {
            var parts = key.split('[');
            parse(parts, parent, 'base', val);
        } else {
            if (!isint.test(key) && isArray(parent.base)) {
                var t = {};
                for (var k in parent.base) t[k] = parent.base[k];
                parent.base = t;
            }
            if (key !== '') {
                set(parent.base, key, val);
            }
        }
        return parent;
    }

    function parseString(str) {
        return reduce(String(str).split(/&|;/), function(ret, pair) {
            try {
                pair = decodeURIComponent(pair.replace(/\+/g, ' '));
            } catch(e) {
                // ignore
            }
            var eql = pair.indexOf('='),
                brace = lastBraceInKey(pair),
                key = pair.substr(0, brace || eql),
                val = pair.substr(brace || eql, pair.length);

            val = val.substr(val.indexOf('=') + 1, val.length);

            if (key === '') {
                key = pair;
                val = '';
            }

            return merge(ret, key, val);
        }, { base: {} }).base;
    }

    function set(obj, key, val) {
        var v = obj[key];
        if (typeof v === 'undefined') {
            obj[key] = val;
        } else if (isArray(v)) {
            v.push(val);
        } else {
            obj[key] = [v, val];
        }
    }

    function lastBraceInKey(str) {
        var len = str.length,
            brace,
            c;
        for (var i = 0; i < len; ++i) {
            c = str[i];
            if (']' == c) brace = false;
            if ('[' == c) brace = true;
            if ('=' == c && !brace) return i;
        }
    }

    function reduce(obj, accumulator){
        var i = 0,
            l = obj.length >> 0,
            curr = arguments[2];
        while (i < l) {
            if (i in obj) curr = accumulator.call(undefined, curr, obj[i], i, obj);
            ++i;
        }
        return curr;
    }

    function isArray(vArg) {
        return Object.prototype.toString.call(vArg) === "[object Array]";
    }

    function keys(obj) {
        var key_array = [];
        for ( var prop in obj ) {
            if ( obj.hasOwnProperty(prop) ) key_array.push(prop);
        }
        return key_array;
    }

    function purl( url, strictMode ) {
        if ( arguments.length === 1 && url === true ) {
            strictMode = true;
            url = undefined;
        }
        strictMode = strictMode || false;
        url = url || window.location.toString();

        return {

            data : parseUri(url, strictMode),

            // get various attributes from the URI
            attr : function( attr ) {
                attr = aliases[attr] || attr;
                return typeof attr !== 'undefined' ? this.data.attr[attr] : this.data.attr;
            },

            // return query string parameters
            param : function( param ) {
                return typeof param !== 'undefined' ? this.data.param.query[param] : this.data.param.query;
            },

            // return fragment parameters
            fparam : function( param ) {
                return typeof param !== 'undefined' ? this.data.param.fragment[param] : this.data.param.fragment;
            },

            // return path segments
            segment : function( seg ) {
                if ( typeof seg === 'undefined' ) {
                    return this.data.seg.path;
                } else {
                    seg = seg < 0 ? this.data.seg.path.length + seg : seg - 1; // negative segments count from the end
                    return this.data.seg.path[seg];
                }
            },

            // return fragment segments
            fsegment : function( seg ) {
                if ( typeof seg === 'undefined' ) {
                    return this.data.seg.fragment;
                } else {
                    seg = seg < 0 ? this.data.seg.fragment.length + seg : seg - 1; // negative segments count from the end
                    return this.data.seg.fragment[seg];
                }
            }

        };

    }
    
    purl.jQuery = function($){
        if ($ != null) {
            $.fn.url = function( strictMode ) {
                var url = '';
                if ( this.length ) {
                    url = $(this).attr( getAttrName(this[0]) ) || '';
                }
                return purl( url, strictMode );
            };

            $.url = purl;
        }
    };

    //purl.jQuery(window.jQuery);

    return purl;

});


 /**
 * Created by Gifer on 20.02.2017.
 */

 (function (root) {

     // Store setTimeout reference so promise-polyfill will be unaffected by
     // other code modifying setTimeout (like sinon.useFakeTimers())
     var setTimeoutFunc = setTimeout;

     function noop() {}

     // Polyfill for Function.prototype.bind
     function bind(fn, thisArg) {
         return function () {
             fn.apply(thisArg, arguments);
         };
     }

     function Promise(fn) {
         if (typeof this !== 'object') throw new TypeError('Promises must be constructed via new');
         if (typeof fn !== 'function') throw new TypeError('not a function');
         this._state = 0;
         this._handled = false;
         this._value = undefined;
         this._deferreds = [];

         doResolve(fn, this);
     }

     function handle(self, deferred) {
         while (self._state === 3) {
             self = self._value;
         }
         if (self._state === 0) {
             self._deferreds.push(deferred);
             return;
         }
         self._handled = true;
         Promise._immediateFn(function () {
             var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;
             if (cb === null) {
                 (self._state === 1 ? resolve : reject)(deferred.promise, self._value);
                 return;
             }
             var ret;
             try {
                 ret = cb(self._value);
             } catch (e) {
                 reject(deferred.promise, e);
                 return;
             }
             resolve(deferred.promise, ret);
         });
     }

     function resolve(self, newValue) {
         try {
             // Promise Resolution Procedure: https://github.com/promises-aplus/promises-spec#the-promise-resolution-procedure
             if (newValue === self) throw new TypeError('A promise cannot be resolved with itself.');
             if (newValue && (typeof newValue === 'object' || typeof newValue === 'function')) {
                 var then = newValue.then;
                 if (newValue instanceof Promise) {
                     self._state = 3;
                     self._value = newValue;
                     finale(self);
                     return;
                 } else if (typeof then === 'function') {
                     doResolve(bind(then, newValue), self);
                     return;
                 }
             }
             self._state = 1;
             self._value = newValue;
             finale(self);
         } catch (e) {
             reject(self, e);
         }
     }

     function reject(self, newValue) {
         self._state = 2;
         self._value = newValue;
         finale(self);
     }

     function finale(self) {
         if (self._state === 2 && self._deferreds.length === 0) {
             Promise._immediateFn(function() {
                 if (!self._handled) {
                     Promise._unhandledRejectionFn(self._value);
                 }
             });
         }

         for (var i = 0, len = self._deferreds.length; i < len; i++) {
             handle(self, self._deferreds[i]);
         }
         self._deferreds = null;
     }

     function Handler(onFulfilled, onRejected, promise) {
         this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
         this.onRejected = typeof onRejected === 'function' ? onRejected : null;
         this.promise = promise;
     }

     /**
      * Take a potentially misbehaving resolver function and make sure
      * onFulfilled and onRejected are only called once.
      *
      * Makes no guarantees about asynchrony.
      */
     function doResolve(fn, self) {
         var done = false;
         try {
             fn(function (value) {
                 if (done) return;
                 done = true;
                 resolve(self, value);
             }, function (reason) {
                 if (done) return;
                 done = true;
                 reject(self, reason);
             });
         } catch (ex) {
             if (done) return;
             done = true;
             reject(self, ex);
         }
     }

     Promise.prototype['catch'] = function (onRejected) {
         return this.then(null, onRejected);
     };

     Promise.prototype.then = function (onFulfilled, onRejected) {
         var prom = new (this.constructor)(noop);

         handle(this, new Handler(onFulfilled, onRejected, prom));
         return prom;
     };

     Promise.all = function (arr) {
         var args = Array.prototype.slice.call(arr);

         return new Promise(function (resolve, reject) {
             if (args.length === 0) return resolve([]);
             var remaining = args.length;

             function res(i, val) {
                 try {
                     if (val && (typeof val === 'object' || typeof val === 'function')) {
                         var then = val.then;
                         if (typeof then === 'function') {
                             then.call(val, function (val) {
                                 res(i, val);
                             }, reject);
                             return;
                         }
                     }
                     args[i] = val;
                     if (--remaining === 0) {
                         resolve(args);
                     }
                 } catch (ex) {
                     reject(ex);
                 }
             }

             for (var i = 0; i < args.length; i++) {
                 res(i, args[i]);
             }
         });
     };

     Promise.resolve = function (value) {
         if (value && typeof value === 'object' && value.constructor === Promise) {
             return value;
         }

         return new Promise(function (resolve) {
             resolve(value);
         });
     };

     Promise.reject = function (value) {
         return new Promise(function (resolve, reject) {
             reject(value);
         });
     };

     Promise.race = function (values) {
         return new Promise(function (resolve, reject) {
             for (var i = 0, len = values.length; i < len; i++) {
                 values[i].then(resolve, reject);
             }
         });
     };

     // Use polyfill for setImmediate for performance gains
     Promise._immediateFn = (typeof setImmediate === 'function' && function (fn) { setImmediate(fn); }) ||
         function (fn) {
             setTimeoutFunc(fn, 0);
         };

     Promise._unhandledRejectionFn = function _unhandledRejectionFn(err) {
         if (typeof console !== 'undefined' && console) {
             console.warn('Possible Unhandled Promise Rejection:', err); // eslint-disable-line no-console
         }
     };

     /**
      * Set the immediate function to execute callbacks
      * @param fn {function} Function to execute
      * @deprecated
      */
     Promise._setImmediateFn = function _setImmediateFn(fn) {
         Promise._immediateFn = fn;
     };

     /**
      * Change the function to execute on unhandled rejection
      * @param {function} fn Function to execute on unhandled rejection
      * @deprecated
      */
     Promise._setUnhandledRejectionFn = function _setUnhandledRejectionFn(fn) {
         Promise._unhandledRejectionFn = fn;
     };

     if (typeof module !== 'undefined' && module.exports) {
         module.exports = Promise;
     } else if (!root.Promise) {
         root.Promise = Promise;
     }

 })(this);

 (function () {
     function _on(event, cb) {
         if (Object.prototype.toString.call( event ) !== "[object String]"){
             console.error("[events.on] Event name must be string");
         } else
         if (eventList.hasOwnProperty(event)){
             //console.error("[events.on] Event %s already exist", event);

             eventList[event].push(cb);
         } else {
             eventList[event] = [cb];
         }
     }

     function _off(event) {
         if (eventList.hasOwnProperty(event)){
             delete eventList[event];
         } else {
             console.warn("[events.off] Unknown event: " + event);
         }
     }

     function _broadcast(event, args, nolog) {
         var res = null;

         // if (Object.prototype.toString.call( event ) !== "[object String]"){
         //     console.warn("[events.broadcast] Event name must be string");
         // } else
         if (!eventList.hasOwnProperty(event)){
             if (!nolog) {
                 console.warn("[events.broadcast] Unknown event: " + event);
             }
         } else {
             eventList[event].forEach(function (cb) {
                 res = cb.apply(null, [null, args || []]);
             });

             if (res !== void(0)) {
                 return res;
             }
         }
     }

     function _http(url, callback, post, data) {
         var res;

         try{
             res = new Promise(function (resolve, reject) {
                 var _request = new XMLHttpRequest();

                 _request.open(post ? "POST" : "GET", url, post);

                 _request.onreadystatechange = function(){
                     if (_request.readyState === 4) {
                         if (_request.status === 200) {
                             if (callback) {
                                 callback(_request.responseText);
                             }

                             resolve(_request.responseText);
                         } else {
                             reject(_request.statusText);
                         }
                     }
                 };

                 _request.send(post ? data : null);
             });

             res.success = res.then;
         } catch (e){
             console.error(e.message);
         }

         return res;
     }

     function changeLocation() { // info
         if ($location){
             $location.data = purl().data;

             $rootScope.$broadcast("onChangeHrefPath", [$location], true);
         }
     }

     var units      = [];
     var moduleList = {}; // Loaded Modules
     var eventList  = {}; // Event Emitter
     var $rootScope = {   // Global Scope
         $on         : _on,
         $off        : _off,
         $emit       : _broadcast,
         $broadcast  : _broadcast,
         isWebClient : location.protocol !== "file:"
     };
     var $location  = null;

     try {
         $location = purl();
     } catch (e){
         console.error(e.message);
     }

     window.mcComponents = {
         start: function () {
             units.forEach(function (unit) {
                 var selectScope = moduleList;

                 selectScope.$rootScope = $rootScope;
                 selectScope.$http      = _http;
                 selectScope.$location  = $location;
                 selectScope.$scope     = { // Local Scope
                     $on       : _on,
                     $off      : _off,
                     $emit     : _broadcast,
                     $broadcast: _broadcast
                 };

                 try {
                     moduleList[unit.module] = new (Function.prototype.bind.apply(window[unit.module], [null].concat(unit.args.map(function (item) {
                         return selectScope[item];
                     }))));
                 } catch (e){
                     console.error(e);
                 }

                 selectScope = null;
             });

             return this;
         },
         units: function (items) {
             units = Object.prototype.toString.call( items ) === "[object Array]" ? items : [];

             return this;
         },
         _broadcast: _broadcast
     };

     (function(eventInfo){
         window[eventInfo[0]](eventInfo[1] + "popstate", changeLocation, false);
     })(window.addEventListener ? ["addEventListener", ''] : ["attachEvent", "on"]);
 })();

 var _messages_ = {};


/**
 * Created by Gifer on 21.02.2017.
 */

function mcStorage($rootScope) {
    "use strict";

    var callBackFunctions = {};

    var globalOptions = {};

    function sendData(data) {
        $rootScope.$broadcast('sendCMDToElectron', [data.cmd].concat(data.values).concat([
            function (res) {
                if (res && callBackFunctions[res.name] && callBackFunctions[res.name].length && res.name !== "null") {
                    callBackFunctions[res.name].shift()(res.data);
                } else if (data && data.cb) {
                    data.cb(res);
                }
            }
        ]));
    }

    // function getBrowserWindowFromWebContents(remote, contents) {
    //     var topContents = contents;
    //
    //     while (topContents.hostWebContents) {
    //         topContents = topContents.hostWebContents;
    //     }
    //
    //     return remote.BrowserWindow.fromWebContents(topContents);
    // }

    function universalStorage() {
        return {
            load: function (serverID, uin, name, cb) {
                if ($rootScope.isWebClient) {
                    try{
                        if (cb) {
                            cb(webix.storage.local.get(name + uin));
                        } else {
                            return webix.storage.local.get(name + uin);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    if (!callBackFunctions[name]) {
                        callBackFunctions[name] = [];
                    }

                    callBackFunctions[name].push(cb);

                    sendData({
                        cmd: mcConst._CMD_.ce_storage_get,
                        values: [
                            serverID,
                            uin,
                            name
                        ],
                        cb: cb
                    });
                }
            },

            remove: function (serverID, uin, name, cb) {
                if ($rootScope.isWebClient) {
                    try {
                        if (cb) {
                            cb(webix.storage.local.remove(name + uin));
                        } else {
                            return webix.storage.local.remove(name + uin);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    sendData({
                        cmd: mcConst._CMD_.ce_storage_remove,
                        values: [
                            serverID,
                            uin,
                            name
                        ],
                        cb: cb
                    });
                }
            },

            save: function (serverID, uin, name, data, cb) {
                if (!data) {
                    this.remove(serverID, uin, name, cb);
                } else
                if ($rootScope.isWebClient) {
                    try {
                        if (cb) {
                            cb(webix.storage.local.put(name + uin, data));
                        } else {
                            return webix.storage.local.put(name + uin, data);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    sendData({
                        cmd: mcConst._CMD_.ce_storage_save,
                        values: [
                            serverID,
                            uin,
                            name,
                            data
                        ],
                        cb: cb
                    });
                }
            }
        };
    }

    function saveOptions(data, cb) {
        globalOptions = mcService.Marge(globalOptions, data || {});

        if (!mcConst.ServerInfo.ID && !$rootScope.isWebClient){
            console.log("mcConst.ServerInfo.ID : " + mcConst.ServerInfo.ID);
        }

        if ($rootScope.isWebClient) {
            var ID = mcConst.ServerInfo.ID + mcConst.UserInfo.UIN + mcConst.storageOpts.MAIN_OPTIONS;

            try{
                if (cb) {
                    cb(webix.storage.local.put(ID, JSON.stringify(globalOptions)));
                } else {
                    return webix.storage.local.put(ID, JSON.stringify(globalOptions));
                }
            } catch (e){
                console.error(e.message);
            }
        } else {
            sendData({
                cmd: mcConst._CMD_.ce_storage_save,
                values: [
                    mcConst.ServerInfo.ID,
                    mcConst.UserInfo.UIN,
                    mcConst.storageOpts.MAIN_OPTIONS,

                    JSON.stringify(globalOptions)
                ],
                cb: cb
            });
        }
    }

    function loadOptions(cb) {
        if (globalOptions && mcService.isObject(globalOptions) && !mcService.isObjectEmpty(globalOptions)) {
            cb && cb(mcService.Marge({}, globalOptions));
        } else

        if ($rootScope.isWebClient) {
            try {
                globalOptions = webix.storage.local.get(mcConst.ServerInfo.ID + mcConst.UserInfo.UIN + mcConst.storageOpts.MAIN_OPTIONS) || {};
            } catch (e){
                console.error(e.message);
            }

            if (!mcService.isObject(globalOptions) && globalOptions !== "") {
                globalOptions = {};
            }

            if (cb) {
                cb(mcService.Marge({}, globalOptions));
            } else {
                return mcService.Marge({}, globalOptions);
            }
        } else {
            sendData({
                cmd: mcConst._CMD_.ce_storage_get,
                values: [
                    mcConst.ServerInfo.ID,
                    mcConst.UserInfo.UIN,
                    mcConst.storageOpts.MAIN_OPTIONS,
                ],
                cb: function (data) {
                    globalOptions = mcService.StringToObj(data.data) || {};

                    cb && cb(mcService.Marge({}, globalOptions));
                }
            });
        }
    }

    $rootScope.Storage = {
        msgTemplate : universalStorage(),
        saveWndSize : function (cb) {
            if (!$rootScope.isWebClient) {
                require("electron").ipcRenderer.invoke('getWindowWH').then(function (resWH){
                    // console.log("setSizes= " + JSON.stringify(resWH));

                    sendData({
                        cmd: mcConst._CMD_.ce_storage_save,
                        values: [
                            null,
                            null,
                            "WindowSizes",
                            resWH, //[w, h]
                        ],
                        cb: cb
                    });
                });
            }
        },
        historyDialogs: {
            load: function (uin, name, cb) {
                if ($rootScope.isWebClient) {
                    try {
                        if (cb) {
                            cb(webix.storage.local.get(name + uin));
                        } else {
                            return webix.storage.local.get(name + uin);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    if (!callBackFunctions[name]) {
                        callBackFunctions[name] = [];
                    }

                    callBackFunctions[name].push(cb);

                    sendData({
                        cmd: mcConst._CMD_.ce_history_get_dialogs,
                        values: [],
                        cb: cb
                    });
                }
            },

            remove: function (uin, name, cb) {
                if ($rootScope.isWebClient) {
                    try {
                        if (cb) {
                            cb(webix.storage.local.remove(name + uin));
                        } else {
                            return webix.storage.local.remove(name + uin);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    sendData({
                        cmd: mcConst._CMD_.ce_history_remove_dialogs,
                        values: [],
                        cb: cb
                    });
                }
            },

            save: function (uin, name, data, cb) {
                if ($rootScope.isWebClient) {
                    try {
                        if (cb) {
                            cb(webix.storage.local.put(name + uin, data));
                        } else {
                            return webix.storage.local.put(name + uin, data);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    sendData({
                        cmd: mcConst._CMD_.ce_history_set_dialogs,
                        values: [
                            data
                        ],
                        cb: cb
                    });
                }
            }
        },
        dialogs     : universalStorage(),
        contactsTree: universalStorage(),
        statistics  : {
            load: function (serverID, uin, cb) {
                if (!$rootScope.isWebClient) {
                    if (!callBackFunctions[name]) {
                        callBackFunctions[name] = [];
                    }

                    callBackFunctions[name].push(cb);

                    sendData({
                        cmd: mcConst._CMD_.ce_statistics_get,
                        values: [
                            serverID,
                            uin,
                        ],
                        cb: cb
                    });
                }
            },

            save: function (serverID, uin, stat, cb) { // mcConst.ServerInfo.ID
                if (!$rootScope.isWebClient) {
                    sendData({
                        cmd: mcConst._CMD_.ce_statistics_save,
                        values: [
                            serverID,
                            uin,
                            stat
                        ],
                        cb: cb
                    });
                }
            }
        },
        options     : {
            load: loadOptions,
            save: saveOptions
        },
        popularSmiles: universalStorage()
    };

    window.mcStorage = $rootScope.Storage;
}

function servicesController(_api) {
    'use strict';

    var hexCodes = ["0","1","2","3","4","5","6","7","8","9","A","B","C","D","E","F"];
    var imgIndex = 1;

    var serviceList = {
        ModalTimer : null,
        noNotify   : false,
        notify     : null,
        idNotify   : 0,

        /**
         * @return {string}
         */
        insertIco : function(ico, css, attr){
            if (serviceList.isObject(attr)){
                var tt = [];

                Object.keys(attr).forEach(function (item) {
                    tt.push(item + "='" + attr[item] + "'")
                });

                attr = tt.join(' ');
            }

            return "<span class='webix_icon " + ico + " " + (css || "fa-lg") + "' " + (attr || "") + "></span>"
        },

        /**
         * @return {boolean}
         */
        checkRights: function(id, rightsList){
            var res = false;

            id = parseInt(id);

            if (!isNaN(id)){
                res = rightsList ? rightsList[id - 1] == "1" : mcConst.MyRightsSet[id - 1];
            }

            return res;
        },

        /**
         * @return {boolean}
         */
        isPlainObject : function(obj) {
            var res = false;

            if (!obj || !serviceList.isObject(obj) || obj.nodeType || obj.setInterval){
                res = false;
            } else
            if (obj.constructor && !obj['constructor'] && !obj.constructor['isPrototypeOf']){
                res = false;
            } else {
                var key;
                
                for ( key in obj ) {}

                res = key === undefined || !!obj[key];
            }

            return res;
        },

        /**
         * @return {object}
         */
        oldMarge : function () {
            var options, name, src, copy, copyIsArray, clone,
                target = arguments[0] || {},
                i = 1,
                length = arguments.length,
                deep = false;

            // Handle a deep copy situation
            if ( typeof target === "boolean" ) {
                deep = target;
                target = arguments[1] || {};
                // skip the boolean and the target
                i = 2;
            }

            // Handle case when target is a string or something (possible in deep copy)
            if ( typeof target !== "object" && typeof target !== "function") {
                target = {};
            }

            // extend jQuery itself if only one argument is passed
            // if ( length === i ) {
            //     target = this;
            //     --i;
            // }

            for ( ; i < length; i++ ) {
                // Only deal with non-null/undefined values
                if ( (options = arguments[ i ]) !== null ) {
                    // Extend the base object
                    for ( name in options ) {
                        src  = target [ name ];
                        copy = options[ name ];

                        // Prevent never-ending loop
                        if ( target === copy ) {
                            continue;
                        }

                        // Recurse if we're merging plain objects or arrays
                        if ( deep && copy && ( serviceList.isPlainObject(copy) || (copyIsArray = serviceList.isArray(copy)) ) ) {
                            if ( copyIsArray ) {
                                copyIsArray = false;
                                clone = src && serviceList.isArray(src) ? src : [];
                            } else {
                                clone = src && serviceList.isPlainObject(src) ? src : {};
                            }

                            // Never move original objects, clone them
                            target[ name ] = serviceList.oldMarge( deep, clone, copy );

                            // Don't bring in undefined values
                        } else if ( copy !== undefined ) {
                            target[ name ] = copy;
                        }
                    }
                }
            }

            // Return the modified object
            return target;
        },

        /**
         * @return {object}
         */
        Marge : function Marge() {
            if (serviceList.isIE()){
                return serviceList.oldMarge.apply(null, arguments);
            } else {
                if (!Object.assign) {
                    Object.defineProperty(Object, 'assign', {
                        enumerable  : false,
                        configurable: true,
                        writable    : true,
                        value       : function(target, firstSource) {
                            'use strict';

                            if (target === undefined || target === null) {
                                throw new TypeError('Cannot convert first argument to object');
                            }

                            var to = Object(target);

                            for (var i = 1; i < arguments.length; i++) {
                                var nextSource = arguments[i];

                                if (nextSource === undefined || nextSource === null) {
                                    continue;
                                }

                                var keysArray = Object.keys(Object(nextSource));

                                for (var nextIndex = 0, len = keysArray.length; nextIndex < len; nextIndex++) {
                                    var nextKey = keysArray[nextIndex];
                                    var desc = Object.getOwnPropertyDescriptor(nextSource, nextKey);

                                    if (desc !== undefined && desc.enumerable) {
                                        to[nextKey] = nextSource[nextKey];
                                    }
                                }
                            }

                            return to;
                        }
                    });
                }

                return Object.assign.apply(null, arguments);
            }
        },

        /**
         * @return {object}
         */
        deepExtend : function(out) {
            out = out || {};

            for (var i = 1; i < arguments.length; i++) {
                var obj = arguments[i];

                if (!obj)
                    continue;

                for (var key in obj) {
                    if (obj.hasOwnProperty(key)) {
                        if (typeof obj[key] === 'object')
                            out[key] = serviceList.deepExtend(out[key], obj[key]);
                        else
                            out[key] = obj[key];
                    }
                }
            }

            return out;
        },

        /**
         * @return {boolean}
         */
        RGBToHex : function (color) {
            var res = false;

            if (color) {
                if (color.substr(0, 1) === '#') {
                    res = color;
                } else {
                    var digits = /(.*?)rgb\((\d+), (\d+), (\d+)\)/.exec(color);

                    var red = parseInt(digits[2]);
                    var green = parseInt(digits[3]);
                    var blue = parseInt(digits[4]);

                    var rgb = blue | (green << 8) | (red << 16);

                    res = digits[1] + '#' + rgb.toString(16);
                }
            }

            return res;
        },

        /**
         * @return {boolean}
         */
        IntToBool : function (Int){
            return serviceList.isBoolean(val) ? val : val == '1';
        },

        /**
         * @return {number}
         */
        BoolToIntEx : function (b){
            return (b === true) ? 0 : -1;
        },

        /**
         * @return {boolean}
         */
        isIE : function (){
            //return '\v'=='v';

            var tmp = document.documentMode, _isIE;

            // Try to force this property to be a string.
            try {
                document.documentMode = "";
            } catch(e){ }

            // If document.documentMode is a number, then it is a read-only property, and so
            // we have IE 8+.
            // Otherwise, if conditional compilation works, then we have IE < 11.
            // Otherwise, we have a non-IE browser.
            _isIE = typeof document.documentMode === "number" || eval("/*@cc_on!@*/!1");

            // Switch back the value to be unobtrusive for non-IE browsers.
            try{document.documentMode = tmp;}
            catch(e){ }

            return _isIE;
        },

        /**
         * @return {boolean}
         */
        isIE6 : function (){
            var gg = navigator['appVersion'];
            var ggg = gg.indexOf("MSIE");
            var gggg = gg.slice(ggg + 5, ggg + 6);

            return gggg === "6";
        },

        /**
         * @return {boolean}
         */
        isFuckingFirefox : function() {
            return typeof InstallTrigger !== 'undefined';
        },

        /**
         * @return {object}
         */
        LessOfTwo : function (itm1, itm2, inString){
            var u1, u2;

            if (parseInt(itm1) < parseInt(itm2)) {
                u1 = itm1;
                u2 = itm2;
            } else {
                u1 = itm2;
                u2 = itm1;
            }

            return inString ? u1 + "_" + u2 : {min:u1, more:u2};
        },

        /**
         * @return {string}
         */
        formatDate : function (formatDate, formatString){
            if (!formatDate) return formatDate;

            var mcDate = serviceList.isString(formatDate);

            if (mcDate) formatDate = formatDate.split('.');

            var nowYY = (new Date()).getFullYear();
            var yyyy = mcDate ? parseInt(formatDate[2]) : formatDate.getFullYear();
            var yy = yyyy.toString().substring(2);
            var m = mcDate ? parseInt(formatDate[1]) : formatDate.getMonth() + 1;
            var mm = m < 10 ? "0" + m : m;
            var d = mcDate ? parseInt(formatDate[0]) : formatDate.getDate();
            var dd = d < 10 ? "0" + d : d;

            var  h = mcDate ? parseInt(formatDate[3]) : formatDate.getHours();
            var  hh = h < 10 ? "0" + h : h;
            var  n = mcDate ? parseInt(formatDate[4]) : formatDate.getMinutes();
            var  nn = n < 10 ? "0" + n : n;
            var  s = mcDate ? parseInt(formatDate[5]) : formatDate.getSeconds();
            var  ss = s < 10 ? "0" + s : s;
            var  zzz = mcDate ? parseInt(formatDate[6] || "0") : formatDate.getMilliseconds();
            
            if (zzz < 10) {
                zzz = "00" + zzz;
            } else
            if (zzz >= 10 && zzz < 100){
                zzz = "0" + zzz;
            }

            var lang = "en";

            if (typeof mcConst === 'object'){
                lang = mcConst.Lang;
            }

            if (typeof require === 'function'){
                window.moment = require('moment');
            }

            var MMM = moment().locale(lang).month(parseInt(m) - 1).format('MMMM');
            var MM  = moment().locale(lang).month(parseInt(m) - 1).format('MMM');

            formatString = formatString.replace(/\?yyyy/, yyyy == nowYY ? "" : yyyy);
            formatString = formatString.replace(/yyyy/, yyyy);
            formatString = formatString.replace(/yy/, yy);
            formatString = formatString.replace(/mm/, mm);
            formatString = formatString.replace(/m/, m);
            formatString = formatString.replace(/dd/, dd);
            formatString = formatString.replace(/d/, d);
            formatString = formatString.replace(/hh/, hh);
            formatString = formatString.replace(/h/, h);
            formatString = formatString.replace(/nn/, nn);
            formatString = formatString.replace(/n/, n);
            formatString = formatString.replace(/ss/, ss);
            formatString = formatString.replace(/s/, s);
            formatString = formatString.replace(/zzz/, zzz);
            formatString = formatString.replace(/MMM/, MMM);
            formatString = formatString.replace(/MM/, MM);

            return formatString;
        },

        /**
         * @return {string}
         */
        DIV : function (x1, x2){
            var y1 = (x1 / x2).toString();
            var idx = y1.indexOf(".");

            return y1.slice(0, (idx !== -1) ? idx : y1.length);
        },

        CopyArray : function (arr){
            return [].concat(arr);
        },

        StringToArray : function (_msg, separator){
            var res =[];
            var idx = -1;

            if (_msg){
                if (serviceList.isIE() === false) {
                    res = _msg.split(separator) || [];
                } else {
                    do {
                        idx = _msg.indexOf(separator);
                        if (idx >= 0) {
                            res.push(_msg.slice(0, idx));
                            _msg = _msg.slice(idx + 1, _msg.length + 1);
                        }
                    } while (idx !== -1);
                }

                if (res.length && res[res.length - 1].length === 0) {
                    res.pop();
                }
            }

            return res;
        },

        GetSubstring : function (str, term){
            var idx = str.indexOf(term);
            return (idx !== -1)? str.slice(0, idx) : str.slice(0, str.length + 1);
        },

        GetSubstringFrom : function (str, term){
            var idx = str.indexOf(term);
            return (idx !== -1)? str.slice(idx + 1, str.length + 1) : str.slice(0, str.length + 1);
        },

        DelSubstring : function (str, term){
            var idx = str.indexOf(term);
            return (idx !== -1)? str.slice(idx + 1, str.length + 1) : "";
        },

        delArrayItem : function(arr, pos, len){
            if ( pos >= 0 ) {
                arr.splice(pos,(len || 1));
            }
        },

        doGetCaretPosition : function (ctrl) {
            var CaretPos = 0;	// IE Support

            if (document.selection) {
                var Sel = document.selection.createRange();

                ctrl.focus ();
                Sel.moveStart ('character', -ctrl.value.length);

                CaretPos = Sel.text.length;
            } else { // Firefox support
                if (ctrl.selectionStart || ctrl.selectionStart == '0'){
                    CaretPos = ctrl.selectionStart;
                }
            }

            return CaretPos;
        },

        inArray: function( elem, arr, i ) {
            var len;

            if ( arr ) {
                len = arr.length;
                i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;

                for ( ; i < len; i++ ) {
                    if ( i in arr && arr[ i ] === elem ) {
                        return i;
                    }
                }
            }

            return -1;
        },

        inArrayB: function (){
            return serviceList.inArray.apply(null, arguments) !== -1;
        },

        inArrayLowerCase: function( elem, arr, i ) {
            var len;

            if ( arr ) {
                len = arr.length;
                i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;

                for ( ; i < len; i++ ) {
                    if ( i in arr && arr[ i ].toLowerCase() === elem.toLowerCase() ) {
                        return i;
                    }
                }
            }

            return -1;
        },

        inArrayNoStrict: function( elem, arr, i ) {
            var len;

            if ( arr ) {
                len = arr.length;
                i = i ? i < 0 ? Math.max( 0, len + i ) : i : 0;

                for ( ; i < len; i++ ) {
                    if ( i in arr && arr[ i ] == elem ) {
                        return i;
                    }
                }
            }

            return -1;
        },

        isArray : function (obj) {
            return Object.prototype.toString.call( obj ) === '[object Array]';
        },

        isString : function (obj) {
            return Object.prototype.toString.call( obj ) === '[object String]';
        },

        isDate : function (obj) {
            return Object.prototype.toString.call( obj ) === "[object Date]";
        },

        isHtmlElement : function (obj) {
            return Object.prototype.toString.call( obj ).indexOf('[object HTML') === 0;
        },

        isNumber : function (obj) {
            return Object.prototype.toString.call( obj ) === '[object Number]';
        },

        isObject : function (obj) {
            return Object.prototype.toString.call( obj ) === '[object Object]';
        },

        isFunction : function (obj) {
            return Object.prototype.toString.call( obj ) === '[object Function]';
        },

        isBoolean : function (obj) {
            return Object.prototype.toString.call( obj ) === '[object Boolean]';
        },

        RandomHash : function (len, isPwd) {
            var letters =  isPwd
                ? 'JjKAEeaMpsTt9QqRxBcDdF2fGgHh3ibC-kL7mNnPV85yr6SUuZ1WwXY4._vz!@#%$&*'
                : 'JjKAEeaMpsTt9QqRxBcDdF2fGgHhI3ibC-kL7mNnOPV85yr6SUuZo1WwXlY40._vz';
            var res = '';

            for (var i=0; i <  len; i++) {
                res += letters[Math.floor(Math.random() * letters.length)];
            }

            return res;
        },

        getRandomInt : function (min, max){
            return Math.floor(Math.random() * (max - min + 1)) + min;
        },

        GetType : function (obj){
            return Object.prototype.toString.call( obj );
        },

        isFormatedLikeIP : function(ip){
            var reg = /((((\d){1,3}\.){3})((\d){1,3}))/; // |(((^|:)([0-9a-fA-F]{0,4})){1,8})

            return reg.test(ip);
        },

        isValidFIO : function (fio){
            var pattern = new RegExp(/^([\x27а-яА-Яa-zA-ZїЇіІєЄ]{2,}|[\x27а-яА-Яa-zA-ZїЇіІєЄ]{2,}\-[\x27а-яА-Яa-zA-ZїЇіІєЄ]{2,})\040[\x27а-яА-Яa-zA-ZїЇіІєЄ]{2,}|[\x27а-яА-Яa-zA-ZїЇіІєЄ]{2,}\040[\x27а-яА-Яa-zA-ZїЇіІєЄ]{2,}$/);
            return pattern.test(fio);
        },

        isValidEmailAddress : function (emailAddress) {
            var pattern = new RegExp(/^(("[\w-\s]+")|([\w-]+(?:\.[\w-]+)*)|("[\w-\s]+")([\w-]+(?:\.[\w-]+)*))(@((?:[\w-]+\.)*\w[\w-]{0,66})\.([a-z]{2,6}(?:\.[a-z]{2})?)$)|(@\[?((25[0-5]\.|2[0-4][0-9]\.|1[0-9]{2}\.|[0-9]{1,2}\.))((25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\.){2}(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[0-9]{1,2})\]?$)/i);
            return pattern.test(emailAddress);
        },   

        isValidIpAddress : function (ip) {
            var reg = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/; // |(((^|:)([0-9a-fA-F]{0,4})){1,8})

            return reg.test(ip);
        },

        isValidIpAddressV6 : function (ip) {
            var reg = /^\s*((([0-9A-Fa-f]{1,4}:){7}([0-9A-Fa-f]{1,4}|:))|(([0-9A-Fa-f]{1,4}:){6}(:[0-9A-Fa-f]{1,4}|((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){5}(((:[0-9A-Fa-f]{1,4}){1,2})|:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3})|:))|(([0-9A-Fa-f]{1,4}:){4}(((:[0-9A-Fa-f]{1,4}){1,3})|((:[0-9A-Fa-f]{1,4})?:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){3}(((:[0-9A-Fa-f]{1,4}){1,4})|((:[0-9A-Fa-f]{1,4}){0,2}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){2}(((:[0-9A-Fa-f]{1,4}){1,5})|((:[0-9A-Fa-f]{1,4}){0,3}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(([0-9A-Fa-f]{1,4}:){1}(((:[0-9A-Fa-f]{1,4}){1,6})|((:[0-9A-Fa-f]{1,4}){0,4}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:))|(:(((:[0-9A-Fa-f]{1,4}){1,7})|((:[0-9A-Fa-f]{1,4}){0,5}:((25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)(\.(25[0-5]|2[0-4]\d|1\d\d|[1-9]?\d)){3}))|:)))(%.+)?\s*$/;
            // var reg = /^(:([0-9a-fA-F]{0,4})){1,8}$/;

            return reg.test(ip);
        },

        isValidHostName : function (ip) {
            // var reg = /^[0-9a-zA-Z\-.]*$/;
            // var reg = new RegExp("^[\\p{L}0-9a-zA-Z\\-.]*$", "u");
            var reg = /^[0-9a-zA-Zа-яА-Я\-.]*$/;

            return reg.test(ip) || serviceList.isValidIpAddressV6(ip);
        },

        isValidIpRange : function(ip){
            var reg = /^((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)(-)((25[0-5]|2[0-4]\d|[01]?\d\d?)\.){3}(25[0-5]|2[0-4]\d|[01]?\d\d?)$/;

            return reg.test(ip);
        },

        isValidIpMask : function(ip){
            var reg = /^(((25([0-5]|\?)|2([0-4]|\?)(\d|\?)|([01]|\?)?(\d|\?)(\d|\?)?)|\*)\.){3}((25([0-5]|\?)|2([0-4]|\?)(\d|\?)|([01]|\?)?(\d|\?)(\d|\?)?)|\*)$/;

            return reg.test(ip);
        },

        isValidMACAddress : function(mac){
            var reg = /^([0-9a-fA-F]{2}([:-]|$)){6}$|([0-9a-fA-F]{4}([.]|$)){3}$/;

            return reg.test(mac);
        },

        isValidPhoneNumber: function (tel){
            var reg = /^(\s*)?(\+)?([- _():=+]?\d[- _():=+]?){10,14}(\s*)?$/;

            return reg.test(tel);
        },

        isHasNoRussianSymbol : function (sText) {
            var pattern = new RegExp(/[a-zA-Z0-9]/i);

            return pattern.test(sText);//(pattern.test(sText) || (sText.search(" ") == -1));
        },

        isSymbolicNumbers : function (sText) {
            var pattern = new RegExp(/[a-zA-Z0-9а-яА-ЯїЇіІєЄ\s]/i);

            return pattern.test(sText);
        },

        isURL : function (_url){
            var u = /(((http|https|ftp|ftps)(:\/\/)?|(:\/\/)(www\.)?|(www\.))[a-zа-я0-9-]+\.[a-zа-я0-9-]{2,6})/gi;

            return u.test(_url);
        },

        trim : function ( str ){
            return str.toString().replace(/^\s+|\s+$/g,'');
        },

        /**
         * @return {string}
         */
        GetUserOS : function (){
            var os = "";
            var appV = navigator.appVersion;
            var oscpu = navigator.oscpu;

            if (serviceList.isIE()){
                os = appV.split(";")[2];
            } else {
                if (oscpu !== null || oscpu !== undefined) {
                    os = oscpu;
                } else os = appV.slice(appV.indexOf("(") + 1, ((appV.indexOf(";") != -1) ? appV.indexOf(";") : appV.indexOf(")")) );
            }

            return navigator.platform + " " + os;
        },

        /**
         * @return {string}
         */
        GetUserLanguage : function (){
            return  (navigator.language       ? navigator.language       : "") +
                    (navigator.systemLanguage ? navigator.systemLanguage : "");
        },

        /**
         * @return {string}
         */
        GetUserRefLink : function (){
            return document.referrer;
        },

        /**
         * @return {string}
         */
        GetUserAgent : function (){
            return navigator.userAgent;
        },

        /**
         * @return {string}
         */
        GetUserBrowser : function (){
            var browser = "";

            if (this.isIE()){
                browser = "IE";
            } else
            if (navigator.appName === "Opera"){
                browser = "Opera";
            } else
            if (navigator.vendor === "Apple Computer, Inc."){
                browser = "Safari";
            } else
            if (navigator.vendor === "Google Inc."){
                browser = "Chrome";
            } else
            if (navigator.oscpu){
                browser = "FireFox";
            } else {
                browser = "Other Brouser";
            }

            return browser;
        },

        pos  : function (FindStr, Src){
            return Src.indexOf(FindStr);
        },

        copy : function (Src, From, Count){
            return (Src) ? Src.slice(From, From + (Count || (Src.length - From))) : "";
        },

        ReplaceURLs : function( txt, options ) {
            var
                SCHEME = "[a-z\\d.-]+://",
                IPV4 = "(?:(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])",
                HOSTNAME = "(?:(?:[^\\s!@#$%^&*()_=+[\\]{}\\\\|;:'\",.<>/?]+)\\.)+",
                TLD  =  "(?:ac|ad|aero|ae|af|ag|ai|al|am|an|ao|aq|arpa|ar|asia|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|biz|bi|bj|bm|" +
                        "bn|bo|br|bs|bt|bv|bw|by|bz|cat|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|coop|com|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|" +
                        "do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|" +
                        "gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|info|int|in|io|iq|ir|is|it|je|jm|jobs|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|mil|mk|ml|mm|mn|mobi|mo|mp|mq|mr|ms|mt|museum|mu|mv|" +
                        "mw|mx|my|mz|name|na|nc|net|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pro|pr|ps|pt|pw|" +
                        "py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|" +
                        "tl|tm|tn|to|tp|travel|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g|" +
                        "xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp|" +
                        "xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw|dev)",
                PORT = "(?::\\d{1,5})?",
                HOST_OR_IP = "(?:" + HOSTNAME + TLD + PORT + "|" + IPV4 + PORT + ")",
                PATH = "(?:[;/][^#?<>()\\s]*)?",
                QUERY_FRAG = "(?:\\?[^#<>\\s]*)?(?:#[^<>\\s]*)?",
                URI1 = "\\b" + SCHEME + "[^<>(\\s|\\)|\\(|\\[|\\])]+", //"[^<>\\s]+",
                URI2 = "\\b" + HOST_OR_IP + PATH + QUERY_FRAG + "(?!\\w)",

                MAILTO = "mailto:",
                EMAIL = "(?:" + MAILTO + ")?[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@" + HOST_OR_IP + QUERY_FRAG + "(?!\\w)",

                URI_RE = new RegExp( "(?:" + URI1 + "|" + URI2 + "|" + EMAIL + ")", "ig" ),
                SCHEME_RE = new RegExp( "^" + SCHEME, "i" );

            var quotes = {
                "'": "`",
                '>': '<',
                ')': '(',
                ']': '[',
                '}': '{',
                'В»': 'В«',
                'вЂє': 'вЂ№'
            };

            var default_options = {
                callback: function( text, href ) {
                    return href ? '<a href="' + href + '" title="' + href + '">' + text + '<\/a>' : text;
                },
                punct_regexp: /(?:[!?.,:;'"]|(?:&|&amp;)(?:lt|gt|quot|apos|raquo|laquo|rsaquo|lsaquo);)$/
            };

            options = options || {};

            // Temp variables.
            var arr,
                i,
                link,
                href,

                // Output HTML.
                html = '',

                // Store text / link parts, in order, for re-combination.
                parts = [],

                // Used for keeping track of indices in the text.
                idx_prev,
                idx_last,
                idx,
                link_last,

                // Used for trimming trailing punctuation and quotes from links.
                matches_begin,
                matches_end,
                quote_begin,
                quote_end;

            // Initialize options.
            for ( i in default_options ) {
                if ( options[ i ] === undefined ) {
                    options[ i ] = default_options[ i ];
                }
            }

            // Find links.
            while ( arr = URI_RE.exec( txt ) ) {

                link = arr[0];
                idx_last = URI_RE.lastIndex;
                idx = idx_last - link.length;

                // Not a link if preceded by certain characters.
                if ( /[\/:]/.test( txt.charAt( idx - 1 ) ) ) {
                    continue;
                }

                // Trim trailing punctuation.
                do {
                    // If no changes are made, we don't want to loop forever!
                    link_last = link;

                    quote_end = link.substr( -1 );
                    quote_begin = quotes[ quote_end ];

                    // Ending quote character?
                    if ( quote_begin ) {
                        matches_begin = link.match( new RegExp( '\\' + quote_begin + '(?!$)', 'g' ) );
                        matches_end = link.match( new RegExp( '\\' + quote_end, 'g' ) );

                        // If quotes are unbalanced, remove trailing quote character.
                        if ( ( matches_begin ? matches_begin.length : 0 ) < ( matches_end ? matches_end.length : 0 ) ) {
                            link = link.substr( 0, link.length - 1 );
                            idx_last--;
                        }
                    }

                    // Ending non-quote punctuation character?
                    if ( options.punct_regexp ) {
                        link = link.replace( options.punct_regexp, function(a){
                            idx_last -= a.length;
                            return '';
                        });
                    }
                } while ( link.length && link !== link_last );

                href = link;

                // Add appropriate protocol to naked links.
                if ( !SCHEME_RE.test( href ) ) {
                    href = ( href.indexOf( '@' ) !== -1 ? ( !href.indexOf( MAILTO ) ? '' : MAILTO )
                        : !href.indexOf( 'irc.' ) ? 'irc://'
                            : !href.indexOf( 'ftp.' ) ? 'ftp://'
                                : 'http://' )
                        + href;
                }

                // Push preceding non-link text onto the array.
                if ( idx_prev != idx ) {
                    parts.push([ txt.slice( idx_prev, idx ) ]);
                    idx_prev = idx_last;
                }

                // Push massaged link onto the array
                parts.push([ link, href ]);
            }

            // Push remaining non-link text onto the array.
            parts.push([ txt.substr( idx_prev ) ]);

            // Process the array items.
            for ( i = 0; i < parts.length; i++ ) {
                html += options.callback.apply( window, parts[i] );
            }

            // In case of catastrophic failure, return the original text;
            return html || txt;
        },

        isObjectEmpty : function (obj){
            var flag = true;

            if (obj && (this.isObject(obj))){
                for (var i in obj){
                    flag = false;
                }
            }

            return flag;
        },

        StringToObj : function (_data){
            var res = null;

            if (this.isString(_data)){
                try {
                    res = JSON.parse(_data);
                } catch(e){
                    // console.error('Error. Can\'t convert to JSON:\n' + _data);
                }
            }

            return res || _data;
        },

        isNewNotificationSupported: function () {
            if (!window.Notification || !Notification.requestPermission)
                return false;
            if (Notification.permission !== 'granted')
                // throw new Error('You must only call this *before* calling Notification.requestPermission(), otherwise this feature detect would bug the user with an actual notification!');
                return false;

            // try {
            //     new Notification('');
            // } catch (e) {
            //     if (e.name === 'TypeError')
            //     return false;
            // }
            return true;
        },

        CreateNotification : function (opt) {
            if (serviceList.notify !== null){
                clearTimeout(serviceList.idNotify);

                serviceList.notify.close();
                serviceList.idNotify = 0;
                serviceList.notify = null;
            }

            if (!serviceList.noNotify && serviceList.isNewNotificationSupported()){
                serviceList.notify = new window.Notification(opt.title || " ", {
                    icon   : mcConst.imagesPath.all + '75x75.png',
                    tag    : opt.tag || "mcNotify",
                    body   : opt.body || "Empty"
                });

                serviceList.idNotify = setTimeout(function(){
                    serviceList.notify.close();
                    serviceList.idNotify = 0;
                    serviceList.notify   = null;
                }, opt.timeOut);

                serviceList.notify.onerror = function(){
                    if (console) {
                        console.warn("permission state = default or denied");
                    }

                    clearTimeout(serviceList.idNotify);

                    serviceList.idNotify = 0;
                    serviceList.notify = null;
                    serviceList.noNotify = true;
                };

                if (opt.onShow){
                    serviceList.notify.onshow = function(){
                        opt.onShow();
                    };
                }

                if (opt.click){
                    serviceList.notify.onclick = function() {
                        clearTimeout(serviceList.idNotify);

                        if (serviceList.notify) {
                            serviceList.notify.close();
                            serviceList.notify = null;
                        }

                        serviceList.idNotify = 0;

                        window.focus();

                        opt.click();
                    };
                }

                if (serviceList.notify.show) {
                    serviceList.notify.show();
                }
            }
        },

        CheckNotification : function (rrr){
            var res = false;

            if (window.webkitNotifications || window.Notification) {
                if ((window.webkitNotifications && window.webkitNotifications.checkPermission() === 0) || Notification.permission === "granted") { // 0 is PERMISSION_ALLOWED
                    res = true;
                    serviceList.noNotify = false;
                } else {
                    if (window.Notification){
                        Notification.requestPermission(function (permission) { // Whatever the user answers, we make sure Chrome stores the information
                            if(!('permission' in Notification)) {
                                Notification.permission = permission;
                            }
                        });
                    } else {
                        window.webkitNotifications.requestPermission(function(ssss){
                            rrr = ssss === 'granted';
                            serviceList.noNotify = !rrr;
                        });
                    }
                }
            } else {
                console.warn("Notifications are not supported for this Browser/OS version yet.");
            }

            return res;
        },

        mobileDetection : {
            Android:function () {
                return navigator.userAgent.match(/Android/i);
            },
            BlackBerry:function () {
                return navigator.userAgent.match(/BlackBerry/i);
            },
            iOS:function () {
                return navigator.userAgent.match(/iPhone|iPad|iPod/i);
            },
            Opera:function () {
                return navigator.userAgent.match(/Opera Mini/i);
            },
            Windows:function () {
                return navigator.userAgent.match(/IEMobile/i);
            },
            any:function () {
                return (this.Android() || this.BlackBerry() || this.iOS() || this.Opera() || this.Windows()) || false;
            }
        },

        MyFormatTime : function (time, short){
            var res = "";

            var ss = parseInt(time % 60);
            var hh = parseInt(time / 3600);
            var nn = parseInt(time / 60) - (hh * 60);

            if (short){
                res = ((nn < 10) ? ("0" + nn) : nn) + ":" + ((ss < 10) ? ("0" + ss) : ss);
            } else {
                res = ((hh < 10) ? ("0" + hh) : hh) + ":" + ((nn < 10) ? ("0" + nn) : nn) + ":" + ((ss < 10) ? ("0" + ss) : ss);
            }

            return res;
        },

        startOfDay : function(day){
            var now = day.getTime();

            return  now - (day.getHours()*1000*60*60 + day.getMinutes()*1000*60 + day.getSeconds()*1000 + day.getMilliseconds());
        },

        storage: {
            get: function () {
                let res;

                try {
                    res = webix.storage.local.get.apply(null, arguments);
                } catch (e) {}
                
                return res;
            },
            put: function () {
                try {
                    webix.storage.local.put.apply(null, arguments);
                } catch (e) {}
            },
            remove: function () {
                try {
                    webix.storage.local.remove.apply(null, arguments);
                } catch (e) {}
            }
        },

        detectLang : function (init){
            init = init === "undefined" || !init ? null : init;
            
            var locale = init || ( (window.navigator.language || window.navigator.userLanguage).slice(0, 2).toLowerCase() );
            var lang = locale;

            if (locale !== 'en' && locale !== 'ru' && locale !== 'uk') {
                locale = MC_RESOURCE.TextSource.www[_api] + 'en';
                lang = 'en';
            } else {
                locale = MC_RESOURCE.TextSource.www[_api] + locale;
            }

            // webix.storage.local.put(mcConst.storageFields.lang, lang);
            serviceList.storage.put(mcConst.storageFields.lang, lang);

            return [locale, lang];
        },

        Lang : function(id, items, term){
            var opt = {};

            if (serviceList.isObject(id)){
                opt = id;
            } else {
                opt = {
                    id   : id,
                    items: items,
                    term : term,
                    html : false
                };
            }

            var res = mcConst[opt.from ? "LMSG_" + opt.from : "LMSG"][opt.id.toString()];

            if (opt.html){
                if (serviceList.isArray(opt.items)){
                    opt.items.forEach(function (t, i) {
                        if (t) {
                            opt.items[i] = t.replace(/</ig, "&lt;");
                        }
                    });
                } else
                if (opt.items) {
                    opt.items = opt.items.replace(/</ig, "&lt;");
                }
            }

            return opt.items ? serviceList.myReplace(res, opt.items, opt.term) : res;
        },

        getElementByClass : function(classList, _node){
            var res  = null;
            var node = _node || document;

            if(document.getElementsByClassName) {
                res = node.getElementsByClassName(classList)
            } else {
                var list = node.getElementsByTagName('*');
                var length = list.length;
                var classArray = classList.split(/\s+/);
                var classes = classArray.length;
                var result = [];

                for (var i = 0; i < length; i++) {
                    for (var j = 0; j < classes; j++)  {
                        if (list[i].className.search('\\b' + classArray[j] + '\\b') != -1) {
                            result.push(list[i]);

                            break
                        }
                    }
                }

                res = result;
            }

            return res;
        },

        myReplace : function(source, _items, term){
            var res = source;
            var items = [];

            if (_items !== void(0) && source){
                var _term = term || "%s";

                if (!serviceList.isArray(_items)) {
                    items.push(_items);
                } else {
                    items = _items;
                }

                if (items.length > 0){
                    res = source.toString().replace(_term, items.shift());
                    res = serviceList.myReplace(res, items, _term);
                }
            }

            return res;
        },

        initResize : function(source, Name){
            mcConst.scrResize = source;
            mcConst.isShowed  = Name;
        },

        showView : function(view){
            if (view && view.className.indexOf('hidden') !== -1) {
                view.className = view.className.replace(' hidden', "");
            }
        },

        hideView : function(view){
            if (view && view.className.indexOf('hidden') === -1) {
                view.className += ' hidden';
            }
        },

        showCurrentView : function(Name, $rootScope, view, options){
            options = options || {};

            if (mcConst.isShowed !== Name) {
                mcConst.isShowed && $rootScope.$broadcast('hide' + mcConst.isShowed);
                $rootScope.$broadcast('setLocation', ['/' + $rootScope.currentRootPath + '/' + Name + '/', options.callBack, options.title]);

                serviceList.showView(view);

                mcConst.isShowed = Name;
            }
        },

        getLangItemsFromList : function(from, to, correct){
            var res = [];

            correct = correct || 0;

            for (var i = from; i <= to; i++){ // 249 492
                res.push({
                    id    : (i - from + correct).toString(),
                    value : serviceList.Lang(i)
                })
            }

            return res;
        },

        getLanguagesItems : function(from, to, correct){
            var res = [];

            correct = correct || 0;

            for (var i = from; i <= to; i++){ // 249 492
                res.push({
                    id    : (i - from + correct).toString(),
                    value : mcConst.LanguagesList[i]
                })
            }

            return res;
        },

        GetFioFormatList : function(){
            return serviceList.getLangItemsFromList(975, 983);/*.sort(function(a, b){
            return serviceList.sortAB(a.value.toLowerCase(), b.value.toLowerCase());
        });*/
        },

        sortAB : function(a,b,invert){
            return (a > b) ? (invert ? -1 : 1) : ((a < b) ? (invert ? 1 : -1) : 0);
        },

        GetCountriesList : function(){
            var res = serviceList.getLanguagesItems(249, 492, 1).sort(function(a, b){
                return serviceList.sortAB(a.value.toLowerCase(), b.value.toLowerCase());
            });

            res.unshift({
                id   : '-1',
                value: serviceList.Lang({id: 140, from: "admin"}) // "140":"Ничего не выбрано",
            });

            return res;
        },

        GetMaritalStatusList : function(){
            return serviceList.getLangItemsFromList(151, 157, 1);
        },

        GetSpokenLangList : function(){
            var res = serviceList.getLanguagesItems(180, 248, 1).sort(function(a, b){
                return serviceList.sortAB(a.value.toLowerCase(), b.value.toLowerCase());
            });

            res.unshift({
                id: '-1',
                value: serviceList.Lang({id: 140, from: "admin"}) // "140":"Ничего не выбрано",
            });

            return res;
        },

        GetInterestsList : function(){
            var res = serviceList.getLangItemsFromList(494, 543, 1);

            res.unshift({
                id: '-1',
                value: serviceList.Lang(140) // "140":"Ничего не выбрано",
            });

            return res;
        },

        GetPastList : function(){
            var res = serviceList.getLangItemsFromList(546, 553, 1);

            res.unshift({
                id: '-1',
                value: serviceList.Lang(140) // "140":"Ничего не выбрано",
            });

            return res;
        },

        GetPastOrgList : function(){
            var res = serviceList.getLangItemsFromList(554, 573, 1);

            res.unshift({
                id: '-1',
                value: serviceList.Lang(140) // "140":"Ничего не выбрано",
            });

            return res;
        },

        GetWeekDays : function(){
            return serviceList.getLangItemsFromList(646, 652);
        },

        SaveSessionSettings : function(login, pwd, servPwd, rm, email){
            if (serviceList.isObject(login)){
                pwd     = login.pwd;
                servPwd = login.servPwd;
                rm      = login.rm;
                login   = login.login;
                // email   = login.Email;
            }

                serviceList.storage.put(mcConst.storageFields.Login, login);

                if (rm){
                    serviceList.storage.put(mcConst.storageFields.Pwd, pwd);
                    serviceList.storage.put(mcConst.storageFields.ServPwd, servPwd);
                    serviceList.storage.put(mcConst.storageFields.Rm, true);

                    // if (mcConst.storageFields.Email) webix.storage.local.put(mcConst.storageFields.Email, email); // for websupport
                } else {
                    serviceList.storage.remove(mcConst.storageFields.Pwd);
                    serviceList.storage.remove(mcConst.storageFields.ServPwd);
                    serviceList.storage.remove(mcConst.storageFields.Rm);

                    // if (mcConst.storageFields.Email) webix.storage.local.remove(mcConst.storageFields.Email); // for websupport
                }

        },

        LoadSessionSettings : function(){
            var res = {};

                res.login   = serviceList.storage.get(mcConst.storageFields.Login);
                res.pwd     = serviceList.storage.get(mcConst.storageFields.Pwd);
                res.servPwd = serviceList.storage.get(mcConst.storageFields.ServPwd);
                res.rm      = serviceList.storage.get(mcConst.storageFields.Rm);

            // if (mcConst.storageFields.Email) res.Email = webix.storage.local.get(mcConst.storageFields.Email);

            return res;
        },

        ClearSessionSettings: function(leaveUIN, noAutoconnectOnReload){
                if (!leaveUIN) {
                    serviceList.storage.remove(mcConst.storageFields.Login);
                }

                serviceList.storage.remove(mcConst.storageFields.Pwd);
                serviceList.storage.remove(mcConst.storageFields.ServPwd);
                serviceList.storage.remove(mcConst.storageFields.Rm);

                if (noAutoconnectOnReload){
                    serviceList.storage.put(mcConst.storageFields.AutoRld, true);
                } else {
                    serviceList.storage.remove(mcConst.storageFields.AutoRld);
                }
        },

        toNormalDate : function (value, needTime){
            if (serviceList.isString(value)) {
                value = value.split('.');
            }

            var date = value[0] + '.' + //day
                value[1] + '.' + //month
                value[2];        //year

            var time = value[3] + ':' + //hour
                value[4] + ':' + //minutes
                value[5];        //seconds

            return date + ((needTime)? " " + time : "");
        },

        sortableDate : function(date, noSec){
            var dd = date.split('.');

            return dd[2] + '.' + dd[1] + '.' + dd[0] + ' ' + dd[3] + ':' + dd[4] + (!noSec ? ':' + dd[5] : '');
        },

        secToTime : function(sec){
            var ss = sec % 60;
            var dd = parseInt(sec / 86400);
            sec = sec - 86400*dd;
            var hh = parseInt(sec / 3600);
            var nn = parseInt(sec / 60) - (hh * 60);

            return (dd) + ':' + ((hh < 10) ? ("0" + hh) : hh) + ":" + ((nn < 10) ? ("0" + nn) : nn) + ":" + ((ss < 10) ? ("0" + ss) : ss);
        },

        formatNumber : function(numb){
            var stNumb = numb.toString();
            var res = [];
            var id = 1;

            for (var i = stNumb.length - 1; i >= 0; i --){
                res.unshift(((id === 3) ? " " : "") + stNumb[i]);

                id = (id === 3) ? 1 : (id + 1);
            }

            return res.join('');
        },

        convertBool : function (val) {
            switch (val) {
                case true:  val = '1'; break;
                case false: val = '0'; break;
                default:    val = val + "";
            }

            return val;
        },

        convertIntToBool : function (val) {
            return serviceList.isBoolean(val) ? val : val == 1;
        },

        clearPlaneObj : function(obj){
            for (var i in obj){
                obj[i] = null;
            }
        },

        Queue : function(lookupTime, loop){
            var self      = this;
            var needRun   = [];
            var canNext   = loop || false;
            var paramList = [];
            var nowRunned = false;
            var time      = lookupTime || 100;

            var lookUp    = null;

            function start(){
                return setInterval(function(){
                    if (!nowRunned && needRun.length > 0){
                        nowRunned = true;

                        var arg = (serviceList.isArray(paramList[0])) ? paramList[0] : [paramList[0]];

                        needRun[0].apply(self, arg);
                    }
                }, time);
            }

            this.next = function(){
                if (needRun.length > 0){
                    needRun.shift();
                    paramList.shift();

                    nowRunned = false;
                } else
                if (!canNext){
                    clearInterval(lookUp);

                    self = null;
                }
            };

            this.addTask = function (qItem, params){
                if (needRun.length === 0 && !nowRunned) {
                    lookUp = start();
                }

                needRun.push(qItem);
                paramList.push(params);

                return needRun.length;
            };
        },

        TaskList : function(_scope){
            var self  = this;
            var list  = [];
            var scope = {};

            this.AddTask = function(task){
                list.push(task);
            };

            this.Next = function(){
                if (list[0]){
                    list.shift().apply(scope, arguments);
                } else {
                    self = null;
                }
            };

            this.Run = function(){
                self.Next();
            };

            scope = serviceList.Marge({
                next : self.Next,
                Next : self.Next
            }, _scope || {});
        },

        transformDataObject : function(source, dependency){
            var res = [];

            if (source && dependency){
                for ( var i = 0; i < source.length; i++){
                    var item = {};

                    for (var j = 0; j < dependency.length; j++){
                        item[dependency[j][1]] = source[i][dependency[j][0]].toString();
                    }

                    res.push(item);
                }
            } else {
                res = source;
            }

            return res;
        },

        networkInterfaces: function(IPVer, IPlist){
            var myType =(IPVer == 0) ? "IPv4" : "IPv6"; // $scope.data.IPVer
            var IPs    = [].concat(IPlist[myType]); // $scope.data.networkInterfaces[myType]

            for (var i = 0; i < IPs.length; i++){
                IPs[i] = {id : IPs[i], value: (( IPs[i] == '0.0.0.0') || (IPs[i] == '::')) ? serviceList.Lang(667) : IPs[i]};// "667": "слушать всё",
            }

            return IPs;
        },

        correctStringWithCRLF : function(val){
            return (serviceList.isString(val)) ? val.replace(/(?!\r)\n/g, '\r\n') : val;
        },

        removeCRFL: function(text){
            return text.toString().replace(/\n/g, '<br>').replace(/\r/g, '');
        },

        initChangesApplyForSettings: function($scope, $rootScope, CMD, applyText, storage, cancelSave){
            var backupItems = {};

            storage = storage || mcConst.dataModels.SettingsNetwork;

            function clearChangedData (){
                $scope.changedData.data.Items    = {};
                $scope.changedData.data.Desc     = "";
                $scope.changedData.data.ShowDesc = false;
            }

            $scope.changedData = {
                data : {
                    Desc   : "",
                    Items  : {},
                    ShowDesc : false,
                    Reopen : false
                },
                clearChangedData: clearChangedData,
                onApply : function() {
                    $scope.data = serviceList.Marge($scope.data, $scope.changedData.data.Items);
                    backupItems = serviceList.Marge({}, $scope.changedData.data.Items);

                    var data = {};

                    if ($scope.changedData.data.Items.hasOwnProperty("TURNPortTCP")){
                        data.turn = $scope.changedData.data.Items["TURNPortTCP"];
                    }
                    if ($scope.changedData.data.Items.hasOwnProperty("WEBTCPPort")){
                        data.web = $scope.changedData.data.Items["WEBTCPPort"];
                    }
                    if ($scope.changedData.data.Items.hasOwnProperty("TCPPort")){
                        data.core = $scope.changedData.data.Items["TCPPort"];
                    }
                    if ($scope.changedData.data.Items.hasOwnProperty("FTPPort")){
                        data.ftp = $scope.changedData.data.Items["FTPPort"];
                    }
                    if ($scope.changedData.data.Items.hasOwnProperty("FTPPortsPool")){
                        data.ftprange = $scope.changedData.data.Items["FTPPortsPool"];
                    }
                    if ($scope.changedData.data.Items.hasOwnProperty("TURNExternalConfig")){
                        data.turnconfig = $scope.changedData.data.Items["TURNExternalConfig"];
                    }

                    if (Object.keys(data).length){
                        $rootScope.$broadcast('SendCMDToServer', [
                            mcConst._CMD_.cs_adm_check_ports,
                            mcConst.SessionID,
                            JSON.stringify(data),
                            function () {
                                $scope.applyChange($scope.data);

                                clearChangedData();
                            }
                        ]);
                    } else {
                        $scope.applyChange($scope.data);

                        clearChangedData();
                    }
                },
                onClose : function(){
                    $scope.$emit("show" + $scope.Name, []);

                    clearChangedData();
                }
            };

            function changeApplyData (id, val){
                $scope.changedData.data.ShowDesc  = true;
                $scope.changedData.data.Items[id] = serviceList.correctStringWithCRLF(val);
                $scope.changedData.data.Desc      = serviceList.Lang(590); // "590": "Список измененных параметров:<br>",

                var item = "";

                for (var i in $scope.changedData.data.Items){
                    item = $scope.changedData.data.Items[i].toString().replace(/\\/g, '\\\\').replace(/</g, "&lt;");

                    $scope.changedData.data.Desc += "<br>" + i + ": <b>" + (
                        (
                            (i.indexOf('PWD') !== -1) ||
                            (i.toLowerCase().indexOf('pass') !== -1) ||
                            (i === "UseConnPass")
                        ) ? "*****" : item ) + "</b>"; // &#092;
                }

                $rootScope.$broadcast('show' + mcConst.dataModels.ApplyPanel, [$scope.changedData]);
            }

            function checkLastSlash(data, where) {
                if (data[where] && ["\\", "/"].indexOf(data[where][data[where].length - 1]) === -1){
                    data[where] += '\\';
                }
                // if (data.BackupsDir && (data.BackupsDir[data.BackupsDir.length - 1] != '\\') && (data.BackupsDir[data.BackupsDir.length - 1] != '/')){
                //     data.BackupsDir += '\\';
                // }
            }

            $scope.change = function(newV, oldV){
                if ($scope.skipChangeNow) return;

                var id = serviceList.copy(this.config.id, 3);
                var valid = true;
                // var restoreValue = false;

                if (newV == undefined){
                    newV = this.getValue();
                }

                if (oldV === undefined){
                    oldV = $rootScope.mcStorage[storage][id];
                }

                if (this.config.pathAlias){
                    newV = serviceList.trim(newV.replace(/\W/ig, ""));

                    this.$setValue(newV);
                } else
                if (this.config.view === "checkbox"){
                    newV = serviceList.convertIntToBool(newV);
                    oldV = serviceList.convertIntToBool(oldV);
                } else
                if (this.config.mcType === "port"){
                    valid = !isNaN(parseInt(newV)) && newV && (newV > 0) && (newV < 65534); //^\d+$/.test(newV);
                } else
                // if (this.config.id === "stgRobotAvatarNum"){
                //     valid = (/^\d+$/.test(newV)) && (newV >= 0) && (newV <= 239);
                // } else
                // if (this.config.id === "stgMaxUploadFileSize"){
                //     newV = serviceList.convertIntToBool(newV);
                // } else
                if (this.config.id === "stgRobotName"){
                    valid = serviceList.trim(newV) !== "";
                } else
                if (this.config.id === "stgExternalIP" || this.config.id === "stwExternalIP"){
                    valid = serviceList.isValidIpAddress(newV) || serviceList.isValidHostName(newV) ? serviceList.trim(newV).indexOf(":") === -1 : false;

                    if (!valid){
                        newV = $rootScope.mcStorage[storage][id];
                    }

                    changeApplyData(id, newV);
                }

                if (valid && (mcConst.isShowed === $scope.Name) && (serviceList.correctStringWithCRLF(newV) !== oldV)){
                    if (this.config.pathAlias && !newV){
                        newV = oldV;
                    }

                    changeApplyData(id, newV);
                } else {
                    if (((this.config.view !== 'text') && (this.config.view !== 'textarea'))) {
                        this.$setValue(oldV);
                    }
                }
            };

            $scope.changeExc = function () {
                if (this.config && this.config.view === 'counter' || this.config.view === 'text') {
                    var newV = this.getValue();
                    var oldV = $scope.data[serviceList.copy(this.config.id, 3)];

                    if (!isNaN(newV)) {
                        $scope.change.apply(this, [newV, oldV]);
                    } else {
                        $scope.change.apply(this, [oldV, 1]);
                        this.$setValue(oldV);
                    }
                }
            };

            $scope.applyChange = function(data){
                // webix.message(applyText($scope.changedData.data.Items));

                if (cancelSave && cancelSave()){
                    return;
                }

                if ($scope._onApply){
                    $scope._onApply(data);
                }

                ["BackupsDir", "FTPPublicDir", "FTPPersonalDir", "FilesDir", "LogsDir"].forEach(function (t) {
                    checkLastSlash(data, t);
                });

                var res = JSON.stringify(data, function(key, value){
                    return value;
                });

                $rootScope.$broadcast('SendCMDToServer', [
                    CMD,
                    mcConst.SessionID,
                    res,
                    function () {
                        webix.message(applyText(backupItems));
                        
                        $rootScope.mcStorage[storage] = serviceList.Marge($rootScope.mcStorage[storage], backupItems || {});
                    }
                ]);

                $rootScope.$broadcast('SendCMDToServer', [
                    mcConst._CMD_.cs_adm_get_server_options,
                    mcConst.SessionID
                ]);
            };
        },

        openNewWindow : function(url, blank){
            var newWindow = window.open(url, blank ? '_blank' : '', blank ? '' : 'menubar=no,resizable=0,directories=no,location=no,toolbar=no,status=no,screenX=0,screenY=0,width=1000,height=600');

            if (newWindow) newWindow.focus();
        },

        onOffToBool : function(val){
            return serviceList.isBoolean(val) ? val : val == 'on';
        },

        InObject : function(item, obj){
            var res = false;

            for(var i in obj) {
                if (item == obj[i]) {
                    res = true;
                    break;
                }
            }

            return res;
        },

        getIdxByIdFromData : function(data, id){
            var res = null;

            for (var i = 0; i < data.length; i ++){
                if (data[i].id == id) {
                    res = i;
                    break;
                }
            }

            return res;
        },

        getIdxByNameFromData : function(data, key, val){
            var res = null;

            for (var i = 0; i < data.length; i ++){
                if ((data[i][key] !== undefined) && (data[i][key] == val)) {
                    res = i;
                    break;
                }
            }

            return res;
        },

        getValueByNameFromData : function(data, key, val){
            var res = "";

            for (var i = 0; i < data.length; i ++){
                if ((data[i][key] !== undefined) && (data[i][key] == val)) {
                    res = data[i][key];
                    break;
                }
            }

            return res;
        },

        oldLicenseKey : function(key){
            var res = false;
            var reg = /^([A-Z0-9]{4}\-){6}[A-Z0-9]{4}$/;

            res = reg.test(serviceList.trim(key));

            if (res) {
                res = serviceList.trim(key);
            }

            return res;
        },

        checkLicenseKey : function(key){
            var res = false;
            var reg = /^([A-F0-9]{4}\-){10}[A-F0-9]{4}$/;

            res = reg.test(serviceList.trim(key));

            if (res) {
                res = serviceList.trim(key);
            }

            return res;
        },

        checkGuestKey : function(key){
            var res = false;
            var reg = /^([A-F0-9]{4}\-){5}[A-F0-9]{4}$/;

            res = reg.test(serviceList.trim(key));

            if (res) {
                res = serviceList.trim(key);
            }

            return res;
        },

        checkQuizKey : function(key){
            var res = false;
            var reg = /^[A-F0-9]{6,8}\-([A-F0-9]{4}\-){3}[A-F0-9]{4}$/;

            res = reg.test(serviceList.trim(key));

            if (res) {
                res = serviceList.trim(key);
            }

            return res;
        },

        renameProperty : function (oldName, newName) {
            if (this.hasOwnProperty(oldName)) {
                this[newName] = serviceList.trim(this[oldName]);

                delete this[oldName];
            }
            return this;
        },

        ListButtonSet: function(opt){
            function add(){
                $$(_idBtn).hide();
                $$(_idBFld).show();

                if (options.actions.beforeAdd){
                    options.actions.beforeAdd();
                }

                if ( options.hideSearchField ) $$(options.hideSearchField).hide();

                if (items.fields[0]) {
                    if (types[options.type] !== types.combo) {
                        items.fields[0].setValue("");
                        if (items.fields[1]) {
                            items.fields[1].setValue("");
                        }
                    }

                    items.fields[0].focus();

                    if (options.actions.preview){
                        options.actions.preview(items.fields[0]);
                    }
                } else {
                    throw new Error('No fields found!');
                }

                if (options.actions.afterShow){
                    options.actions.afterShow(items.fields);
                }
            }

            function save (){
                if (options.actions.save && options.actions.save.apply(null, items.fields)){
                    back();
                }
            }

            function back (){
                if (items.fields[0] && items.fields[0].hasOwnProperty('getPopup')){
                    items.fields[0].getPopup().hide();
                }

                $$(_idBFld).hide();
                $$(_idBtn).show();

                if ( options.hideSearchField ) $$(options.hideSearchField).show();

                if (options.actions.afterClose){
                    options.actions.afterClose();
                }
            }

            function del(){
                var _list        = $$(options.list);
                var selectedItem = _list.getSelectedItem();

                if (selectedItem && options.actions.del){
                    var len     = selectedItem[options.selectedItem] ? selectedItem[options.selectedItem].length : 0;
                    var delText = serviceList.Lang(1078, len > 15 ? serviceList.copy(selectedItem[options.selectedItem], 0, 15) + "..." : selectedItem[options.selectedItem]); // "1078":"Удалить %s из списка?",

                    if (serviceList.isArray(selectedItem)){
                        delText = serviceList.Lang(1142); // "1142":"Удалить отмеченные элементы?",
                    }

                    options.regShowWindow(serviceList.RandomHash(4), webix.confirm({
                        type  : "confirm-warning",
                        text  : delText,
                        ok    : serviceList.Lang(581), // "581": "Удалить",
                        cancel: serviceList.Lang(103), // "103": "Отмена",
                        callback:function(yes){
                            if (yes){
                                options.actions.del(selectedItem, _list);

                                if (serviceList.isArray(selectedItem)){
                                    for (var i = 0; i < selectedItem.length; i ++){
                                        _list.remove(selectedItem[i].id);
                                    }

                                    webix.message(serviceList.Lang(1506)); // "1506":"Элементы удалены",
                                } else {
                                    webix.message(serviceList.Lang(1077, selectedItem[options.selectedItem])); // "1077":"%s удален",

                                    _list.remove(selectedItem.id);
                                }

                                if (options.actions.afterDel) options.actions.afterDel(_list);
                            }
                        }
                    }));
                }
            }

            function clear(){
                //if ($$(options.list).serialize().length){
                options.regShowWindow(serviceList.RandomHash(4), webix.confirm({
                    type  : "confirm-warning",
                    text  : serviceList.Lang(1072),// "1072":"Очистить список?",
                    ok    : serviceList.Lang(581), // "619": "Очистить",
                    cancel: serviceList.Lang(103), // "103": "Отмена",
                    callback:function(yes){
                        if (yes){
                            if (options.actions.clear()){
                                $$(options.list).clearAll();
                            }
                        }
                    }
                }));
                //}
            }

            function afterRender(){
                // items.fields[0] = this;

                webix.UIManager.addHotKey("Enter", function() {
                    save();

                    return false;
                }, this);

                webix.UIManager.addHotKey("Esc", function() {
                    back();

                    return false;
                }, this);
            }
            
            function edit() {
                var _list        = $$(options.list);
                var selectedItem = _list.getSelectedItem();

                if (selectedItem && options.actions.save) {
                    $$(_idBtn).hide();
                    $$(_idBFld).show();

                    if ( options.hideSearchField ) $$(options.hideSearchField).hide();

                    items.fields[0].setValue(selectedItem.IP);
                    items.fields[1].setValue(selectedItem.ClientType);
                    items.fields[2].setValue(selectedItem.Comment);
                    items.fields[3] = selectedItem.id;

                    items.fields[0].focus();

                    if (options.actions.preview){
                        options.actions.preview(items.fields[0]);
                    }

                    if (options.actions.afterShow){
                        options.actions.afterShow(items.fields);
                    }
                }
            }

            var types   = {
                'textComment'   : 1,
                '1'             : 1,
                'text'          : 2,
                '2'             : 2,
                'combo'         : 3,
                '3'             : 3,
                'textRichselectComment' : 4,
                '4'             : 4,
                'textRichselectCommentEdit' : 5,
                '5'             : 5
            };

            var options = serviceList.Marge({
                type   : types.textComment,
                idBtn  : serviceList.RandomHash(10),
                gravity: 1,
                actions: {
                    del    : null,
                    clear  : null,
                    save   : null,
                    preview: null,
                    enable : null,
                    disable: null,
                    beforeAdd: null,
                    afterClose: null
                },
                text : {
                    field1 : "", // "1075":"Введите IP, маску или диапазон",
                    field2 : "", // serviceList.Lang(1032) "1032":"Комментарий:",
                    field3 : ""  // serviceList.Lang(1032) "1032":"Комментарий:",
                },
                visibleBtns: {
                    add: true,
                    del: true,
                    cls: true
                },
                titleBtns: {
                    add: "",
                    del: "",
                    cls: "",
                    save: "",
                    cancel: "",
                },
                list : null,
                listData : [],
                regShowWindow: null,
                selectedItem: 'value'
            }, opt || {});

            var res     = null;

            if (serviceList.isFunction(options._back)){
                options._back(back);
            }

            if (!options.regShowWindow){
                var errtext = 'Needs \"regShowWindow\" for show popup windows in ' + options.list;

                console.error(errtext);

                webix.message({ type:"error", text: errtext, expire : mcConst.errMessageExpire * 2});
            } else {
                var _idBtn  = options.idBtn;
                var _idBFld = serviceList.RandomHash(10);
                var items   = {
                    fieldBlock: null,
                    fields    : [],
                    list      : null
                };

                res = { gravity: options.gravity, rows : [
                    { id: _idBtn, cols: [] // блок с кнопками: добавить удалить очистить
                        .concat([{ view: "button", value: options.titleBtns.add || serviceList.Lang(50), hidden: !options.visibleBtns.add, click: add, css: "applyButton"}]) // "50" : "Добавить",
                        .concat([{ view: "button", value: options.titleBtns.del || serviceList.Lang(52), hidden: !options.visibleBtns.del, click: del, type: "danger"}])     // "52" : "Удалить",
                        .concat(types[options.type] === types.textRichselectCommentEdit ? [{ view: "button", value: serviceList.Lang(51), hidden: !options.visibleBtns.del, click: edit}] : [])    // "51" : "Изменить",
                        .concat(options.visibleBtns.cls ? [{ view: "button", value: options.titleBtns.cls || serviceList.Lang(619), hidden: !options.visibleBtns.cls,click: clear}] : []) // "619": "Очистить",
                    },
                    { id: _idBFld, hidden: true, rows: [{}]} // появляющийся блок
                ], on: {
                    onShow: function (){
                        
                    }
                }};

                res.emit = {
                    add : add,
                    edit: edit,
                    del : del
                };

                switch (types[options.type]){
                    case types.textComment:
                        res.rows[1].rows = [
                            { view: "text", placeholder: options.text.field1, on: { onAfterRender: function() { items.fields[0] = this; afterRender.apply(this, arguments); } } },
                            { cols: [
                                { view: "text", placeholder: options.text.field2, // id: "ipfAcceptAddComment",
                                    on: { onAfterRender: function() { items.fields[1] = this; afterRender.apply(this, arguments); } }
                                },
                                { view: "button", value: options.titleBtns.save || serviceList.Lang(104), width: 100, click: save}, // "104": "Сохранить",
                                { view: "button", value: options.titleBtns.cancel || serviceList.Lang(103), width: 100, click: back, type: "danger"}  // "103": "Отмена",
                            ]}
                        ];
                    break;

                    case types.text:
                        res.rows[1].cols = [
                            { view: "text", placeholder: options.text.field1, on: { onAfterRender: function() { items.fields[0] = this; afterRender.apply(this, arguments); } } },// "1075":"Введите IP, маску или диапазон",
                            { view: "button", value: options.titleBtns.save || serviceList.Lang(104), width: 100, click: save}, // "104": "Сохранить",
                            { view: "button", value: options.titleBtns.cancel || serviceList.Lang(103), width: 100, click: back, type: "danger"} // "103": "Отмена",
                        ];
                    break;

                    case types.combo:
                        res.rows[1].cols = [
                            { view: "combo", options: options.listData, on: { onAfterRender: function() { items.fields[0] = this; afterRender.apply(this, arguments); } } },
                            { view: "button", value: options.titleBtns.save || serviceList.Lang(104), width: 100, click: save, css: "applyButton"}, // "104": "Сохранить",
                            { view: "button", value: options.titleBtns.cancel || serviceList.Lang(103), width: 100, click: back, type: "danger"} // "103": "Отмена",
                        ];
                    break;

                    case types.textRichselectComment:
                        res.rows[1].rows = [
                            { cols: [
                                { view: "text", placeholder: options.text.field1, on: { onAfterRender: function() { items.fields[0] = this; afterRender.apply(this, arguments); } } },
                                { view: "richselect", placeholder: options.text.field2, options: options.listData, on: { onAfterRender: function() { items.fields[1] = this } } }
                            ]},
                            { cols: [
                                { view: "text", placeholder: options.text.field3, // id: "ipfAcceptAddComment",
                                    on: { onAfterRender: function() { items.fields[2] = this; afterRender.apply(this, arguments); } }
                                },
                                { view: "button", value: options.titleBtns.save || serviceList.Lang(104), width: 100, click: save}, // "104": "Сохранить",
                                { view: "button", value: options.titleBtns.cancel || serviceList.Lang(103), width: 100, click: back, type: "danger"}  // "103": "Отмена",
                            ]}
                        ];
                    break;

                    case types.textRichselectCommentEdit:
                        res.rows[1].rows = [
                            { cols: [
                                { view: "text", placeholder: options.text.field1, on: {
                                    onAfterRender: function() {
                                        items.fields[0] = this;
                                        afterRender.apply(this, arguments);
                                    } } },
                                { view: "richselect", placeholder: options.text.field2, options: options.listData, on: {
                                    onAfterRender: function() {
                                        items.fields[1] = this
                                    } } }
                            ]},
                            { cols: [
                                { view: "text", placeholder: options.text.field3, // id: "ipfAcceptAddComment",
                                    on: { onAfterRender: function() { items.fields[2] = this; afterRender.apply(this, arguments); } }
                                },
                                { view: "button", value: options.titleBtns.save || serviceList.Lang(104), width: 100, click: function () {save(types[options.type])}}, // "104": "Сохранить",
                                { view: "button", value: options.titleBtns.cancel || serviceList.Lang(103), width: 100, click: back, type: "danger"}  // "103": "Отмена",
                            ]}
                        ];
                    break;
                }
            }

            return res;
        },

        findItemInArrayOfObj: function( arr, val, id, lowerCase ){
            for (var i = 0; i < arr.length; i++){
                if (!id){
                    for (var j in arr[i]){
                        if (lowerCase){
                            if (arr[i][j].toLowerCase() == val.toLowerCase()){
                                return i;
                            }
                        } else
                        if (arr[i][j] == val){
                            return i;
                        }
                    }
                } else
                if (lowerCase){
                    if (arr[i][id].toLowerCase() == val.toLowerCase()){
                        return i;
                    }
                } else
                if (arr[i][id] == val){
                    return i;
                }
            }

            return -1;
        },

        ContextMenu : function(_master, oldMenu, cb, onShow, removePrev, addItems, itemsCB){
            function clearSelection() {
                if (master.unselectAll){
                    master.unselectAll()
                } else {
                    master.clearSelection();
                }
            }

            function selectAll (){
                if (master.openAll){
                    master.openAll();
                }

                // clearSelection();

                if (master.selectAll){
                    master.selectAll();
                } else {
                    master.selectRange(master.getFirstId(), master.getLastId());
                }
            }

            var master = _master || {};

            if (oldMenu && removePrev) {
                oldMenu.destructor();
            }

            if (!master.contextTitle) {
                master.contextTitle = {};
            }

            var contextMenu = !oldMenu || (oldMenu && removePrev) ? {
                view  : "context",
                width : 220,
                master: master,
                body  : { rows: []},
                on: {
                    onShow: function() {
                        if (onShow) onShow.apply(this, []);
                    }
                }
            } : [];

            var items = [
                { view: "button", value: serviceList.Lang(master.contextTitle.all || 1105), click: function(){ // "1105":"Выделить все",
                    var context = this.getTopParentView();

                    if (cb) cb.call(context.getContext().obj, 'beforeSelectAll');

                    selectAll();

                    if (cb) cb.call(context.getContext().obj, 'selectAll');

                    context.hide();
                }},
                { view: "button", value: serviceList.Lang(master.contextTitle.clear || 1106), click: function(){ // "1106":"Снять выделение",
                    var context = this.getTopParentView();

                    if (cb) cb.call(context.getContext().obj, 'beforeClear');

                    clearSelection();

                    if (cb) cb.call(context.getContext().obj, 'clear');

                    context.hide();
                }},
                { view: "button", value: serviceList.Lang(master.contextTitle.invert || 1107), click: function(){ // "1107":"Инвертировать выделение",
                    var selectedItems = master.getSelectedItem();
                    var context = this.getTopParentView();

                    if (cb) cb.call(context.getContext().obj, 'beforeInvert');

                    if (serviceList.isObject(selectedItems)){
                        selectedItems = [selectedItems];
                    }

                    selectAll();

                    for (var i = 0; selectedItems && i < selectedItems.length; i ++){
                        master.unselect(selectedItems[i].id);
                    }

                    if (cb) cb.call(context.getContext().obj, 'invert');

                    context.hide();
                }}
            ].concat(addItems && addItems.length && itemsCB && itemsCB.length === addItems.length
                ? addItems.map(function (item, id){
                    item.click = function () {
                        var selectedItems = master.getSelectedItem(true);
                        
                        itemsCB[id](selectedItems);

                        this.getTopParentView().hide();
                    }

                    return item;
                })
                : []
            );

            if (oldMenu === true && !removePrev){
                contextMenu = items;
            } else {
                contextMenu.body.rows = contextMenu.body.rows.concat(items);
                
                contextMenu = webix.ui(contextMenu);
            }

            return contextMenu;
        },

        getAllUserForFillList: function(list, cb, $rootScope){
            var usersCash = $rootScope.mcStorage[mcConst.dataModels.UserManager];

            if (usersCash && usersCash.Params === "All" && !list.length){
                cb.apply(usersCash, [usersCash.Users]);
            } else
            if (!usersCash || usersCash.Params !== "All" || (list.length > 0 && usersCash.Users.length !== list.length)){
                $rootScope.$broadcast('getAllUserList', [false, function() {
                    usersCash = $rootScope.mcStorage[mcConst.dataModels.UserManager];

                    cb.apply(usersCash, [usersCash.Users]);
                }]);
            }
        },

        getAllUserWithParams: function(cb, $rootScope, params){
            var usersCash = $rootScope.mcStorage[mcConst.dataModels.UserManager];

            if (usersCash && usersCash.Params === "All" &&  params === "All"){
                cb.apply(usersCash, [usersCash.Users]);
            } else {
                $rootScope.$broadcast('SendCMDToServer', [
                    mcConst.lockInterface,
                    mcConst._CMD_.cs_adm_list_users,
                    mcConst.SessionID,
                    !params ? 'All' : params.Params || 'All',
                    !params ? mcConst.userFields : params.Filters || mcConst.userFields,
                    function (data) {
                        $rootScope.mcStorage.UserManager = data;

                        cb.apply(data, [data.Users]);
                    }
                ]);
            }
        },

        getMcSettings: function($rootScope, show, always){
            if (!$rootScope.mcStorage.SettingsNetwork || always){
                $rootScope.$broadcast('SendCMDToServer', [
                    mcConst.lockInterface,
                    mcConst._CMD_.cs_adm_get_server_options,
                    mcConst.SessionID,

                    function (data){
                        $rootScope.mcStorage.SettingsNetwork = data;

                        $rootScope.$broadcast('SendCMDToServer', [
                            mcConst.lockInterface,
                            mcConst._CMD_.cs_adm_get_server_info,
                            mcConst.SessionID,

                            function( data ){
                                $rootScope.mcStorage.ServerInfo = data;

                                if ( show ) show();
                            }
                        ]);
                    }
                ]);
            } else {
                if ( show ) show();
            }
        },

        getTitle: function(path){
            var res = "";

            switch (path){
                case "ServerInfo":      res = serviceList.Lang(10);   break; // "10" : "Информация о сервере",
                case "UserManager":     res = serviceList.Lang(49);   break; // "49" : "Список пользователей",
                case "UserProfile":     res = serviceList.Lang(1484); break; // "1484":"Профиль пользователя",
                case "GroupRights":     res = serviceList.Lang(57);   break; // "57" : "Группы прав"
                case "GroupUsers":      res = serviceList.Lang(916);  break; // "916":"Пользователи групп",
                case "Contacts":        res = serviceList.Lang(587);  break; // "587": "Общий список контактов",
                case "WorkPositions":   res = serviceList.Lang(588);  break; // "588": "Должности компании",
                case "WebSupport":      res = serviceList.Lang(938);  break; // "938":"Web-поддержка",
                case "MyChatGuest":     res = serviceList.Lang(945);  break; // "945":"MyChat Guest",
                case "LicenseMyChat":   res = serviceList.Lang(944);  break; // "944":"MyChat Server",
                case "LicenseGuest":    res = serviceList.Lang(945);  break; // "945":"MyChat Guest",
                case "UsersImports":    res = serviceList.Lang(960);  break; // "960":"Импорт пользователей",
                case "Operators":       res = serviceList.Lang(817);  break; // "817":"Модераторы",
                case "Bans":            res = serviceList.Lang(1029); break; // "1029":"Забаненные пользователи",
                case "AutoChannels":    res = serviceList.Lang(1039); break; // "1039":"Автосоздаваемые конференции",
                case "RemoteSettings":  res = serviceList.Lang(1329); break; // "1329":"Дистанционные настройки клиента",
                case "History":         res = serviceList.Lang(1134); break; // "1134":"Просмотр протоколов",
                case "FilterAntiflood": res = serviceList.Lang(1057); break; // "1057":"Антифлуд",
                case "FilterBadWords":  res = serviceList.Lang(1058); break; // "1058":"Плохие слова",
                case "FilterIP":        res = serviceList.Lang(1059); break; // "1059":"IP фильтры",
                case "FilterMAC":       res = serviceList.Lang(1060); break; // "1060":"MAC фильтры",
                case "FilterNames":     res = serviceList.Lang(1061); break; // "1061":"Блокировки названий",
                case "SettingsNetwork": res = serviceList.Lang(591);  break; // "591": "Сетевые настройки MyChat",
                case "SettingsInfo":    res = serviceList.Lang(596);  break; // "596": "Информация",
                case "SettingsLogs":    res = serviceList.Lang(598);  break; // "598": "Протоколирование",
                case "SettingsFTP":     res = serviceList.Lang(599);  break; // "599": "FTP файловый сервер",
                case "SettingsWEB":     res = serviceList.Lang(600);  break; // "600": "WEB-сервисы",
                case "SettingsBackup":  res = serviceList.Lang(601);  break; // "601": "Резервные копии",
                case "SettingsService": res = serviceList.Lang(602);  break; // "602": "Обслуживание",
                case "SettingsAPI":     res = serviceList.Lang(603);  break; // "603": "Integration API",
                case "SettingsSMTP":    res = serviceList.Lang(921);  break; // "921":"Настройка SMTP",
                case "Others":          res = serviceList.Lang(672);  break; // "672": "Дополнительно",
            }

            return res;
        },

        formatDeptList: function( data ){
            var separator = '';
            var levels = {};
            var i = 0;
            var res = [];
            var normalData = [];
            var idx;
            var deptList = [];

            while (data.length > 0 || i < data.length){
                if (data[i].ParentID == 0) {
                    normalData.push(data[i]);

                    data.splice(i, 1);
                } else {
                    idx = serviceList.findItemInArrayOfObj(normalData, data[i].ParentID, "GroupID");

                    if (idx == -1) {
                        i++;
                    } else {
                        normalData.push(data[i]);

                        data.splice(i, 1);
                    }
                }

                if (data.length > 0 && i == data.length){
                    i = 0;
                }
            }

            for (i = 0; i < normalData.length; i++){
                if (normalData[i].ParentID == 0) {
                    levels[normalData[i].GroupID] = 0;
                } else {
                    levels[normalData[i].GroupID] = levels[normalData[i].ParentID] + 1;
                }

                separator = new Array(levels[normalData[i].GroupID] + 1).join('&#151;');

                res.push({
                    id: normalData[i].GroupID,
                    value: separator + normalData[i].GroupName
                });
            }

            for (i = 0; i < normalData.length; i++){
                if (normalData[i].ParentID == 0){
                    deptList.push(res[i]);
                } else {
                    deptList.splice(serviceList.findItemInArrayOfObj(deptList, normalData[i].ParentID, "id") + 1, 0, res[i]);
                }
            }

            return deptList;
        },

        copyToBuffer: function(){
            setTimeout(function(){
                webix.message(serviceList.Lang(1483)); // "1483":"Лог скопирован в буфер обмена!",
            }, 10);

            return $$("logsContainer").$view.innerText.replace(/\n/g, '\r\n');
        },

        convertDomainToDn : function(dn){
            if (dn.indexOf('=') === -1){
                dn = dn.split('.');

                for (var i = 0; i < dn.length; i++){
                    dn[i] = 'DC=' + dn[i];
                }

                dn = dn.join(',').toUpperCase();
            } else {
                dn = dn.toUpperCase();
            }

            return dn;
        },

        convertDataToTree: function(data, withMe){
            function add(current, parent){
                var item = null;

                if (current.IsGroup){
                    item = {
                        DisplayName: current.GroupName || current.DisplayName,
                        id         : current.ID || current.id,
                        ParentID   : current.ParentID,
                        GroupID    : current.GroupID,
                        IsGroup    : true,
                        folder     : true,
                        data       : current.data || []
                    };
                } else {
                    if (!withMe && current.UIN == mcConst.UserInfo.UIN){
                        return false;
                    }

                    item = {
                        DisplayName: current.DisplayName,
                        id         : current.ID || current.id,
                        ParentID   : current.ParentID,
                        State      : current.State,
                        onlineState: current.onlineState,
                        UIN        : current.UIN,
                        TeamLead   : current.TeamLead
                    };
                }

                parent.push(item);

                return true;
            }

            var res = [];
            var structure = [];
            var _data = [].concat(data);
            var i = 0;
            var needAdd = null;

            while (_data.length > 0){
                if (_data[i].ParentID == -1){
                    _data.splice(i, 1);
                } else
                if (_data[i].ParentID == 0){
                    needAdd = add(_data[i], res);
                    if (needAdd) add(res[res.length - 1], structure);

                    _data.splice(i, 1);
                } else {
                    var idx = serviceList.findItemInArrayOfObj(structure, _data[i].ParentID, "GroupID");

                    if (idx >= 0){
                        needAdd = add(_data[i], structure);
                        if (needAdd) add(structure[structure.length - 1], structure[idx].data);

                        _data.splice(i, 1);
                    } else {
                        i++;
                    }
                }

                if (i == _data.length) {
                    i = 0;
                }
            }

            return res;
        },

        hideFrame: function(id){
            var view = document.getElementById(id);

            if (view){
                view.style.display = 'none';
            }
        },

        showFrame: function(id, inline){
            var view = document.getElementById(id);

            if (view){
                view.style.display = (inline ? 'inline-' : '') + 'block';
            }
        },

        getFrame: function(id, src){
            var res = src || document.getElementById(id);

            if (res && !res.showFrame && !res.hideFrame) {
                res.showFrame = function() {
                    serviceList.showFrame(id) };
                res.hideFrame = function() {
                    serviceList.hideFrame(id) };
            }

            return res;
        },

        convertObjToArray: function(obj, keyToField, copy){
            var res = [];

            for (var i in obj){
                if (serviceList.isObject(obj[i])){
                    if (keyToField) obj[i][keyToField] = i;

                    res.push(copy ? serviceList.Marge({}, obj[i]) : obj[i] );
                }
            }

            return res;
        },

        getStateStatuses: function(state){
            return {
                Online  : state.Online  ? serviceList.StringToArray(state.Online, ',') : serviceList.StringToArray(state.Users, ','),
                Offline : state.Offline ? serviceList.StringToArray(state.Offline, ',') : [],
                States  : state.States  ? serviceList.StringToArray(state.States, ',') : serviceList.StringToArray(state.Statuses, ',')
            };
        },

        addEvent : function(object, type, callback) {
            if (object == null || typeof(object) == 'undefined') return;
            if (object.addEventListener) {
                object.addEventListener(type, callback, false);
            } else if (object.attachEvent) {
                object.attachEvent("on" + type, callback);
            } else {
                object["on"+type] = callback;
            }
        },

        getWindowSize: function(){
            // var wndWidth = 0;
            // var wndHeight = 0;

            /*if (document.documentElement && (document.documentElement.clientWidth || document.documentElement.clientHeight)) {
                wndWidth = document.documentElement.clientWidth;
                wndHeight = document.documentElement.clientHeight;
            } else {
                if (typeof (window.innerWidth) == 'number') {
                    wndWidth = window.innerWidth;
                    wndHeight = window.innerHeight;
                } else
                if (document.body && (document.body.clientWidth || document.body.clientHeight)) {
                    wndWidth = document.body.clientWidth;
                    wndHeight = document.body.clientHeight;
                }
            }*/

            // return {
            //     width  : wndWidth,
            //     height : wndHeight
            // };
            let {
                    width,
                    height,
                    widthOrig = null,
                    heightOrig = null,
                    scale,
                    offsetTop,
                    offsetLeft,
                    offsetTopOrig = null,
                    offsetLeftOrig = null,
                    pageLeft,
                    pageTop,
                    pageLeftOrig = null,
                    pageTopOrig = null,
                } = window.visualViewport;

            return Object.fromEntries(
                Object.entries({
                    width,
                    widthOrig,
                    height,
                    heightOrig,
                    scale,
                    offsetTop,
                    offsetTopOrig,
                    offsetLeft,
                    offsetLeftOrig,
                    pageLeft,
                    pageLeftOrig,
                    pageTop,
                    pageTopOrig,
                }).filter(([k, v]) => v !== null));
        },

        convertDnArrayToTreeTable: function(dnList, base){
            var res = [];
            var dnStructure = {};
            var dn = "distinguishedName";

            function createStructure(obj, structure, parent){
                function add (arr, item){
                    var link = {
                        value: item
                    };

                    if (structure.length === 0){
                        link = serviceList.Marge(link, obj);
                    } else {
                        // link['$row'] = "value";
                        // link['$parent'] = 0;
                        link['open'] = true;
                        link['data'] = [];
                    }

                    arr.push(link);

                    return link;
                }

                if (structure.length){
                    var item = structure.pop().replace('CN=', '');

                    parent = parent || res;

                    var idx = serviceList.findItemInArrayOfObj(parent, item, "value");

                    createStructure(obj, structure, idx === -1 ? add(parent, item).data : parent[idx].data);
                }
            }

            for (var i = 0; i < dnList.length; i++){
                dnList[i][dn] = dnList[i][dn].replace(',' + base, '');

                if (!dnStructure[dnList[i][dn]]){
                    dnStructure[dnList[i][dn]] = dnList[i][dn].split(',');

                    createStructure(dnList[i], [].concat(dnStructure[dnList[i][dn]]), null);
                }
            }

            return res;
        },

        makeIceServers: function (data, isWeb){
            /** @namespace data.IPList*/
            /** @namespace data.TURNPort*/
            /** @namespace data.Credential */
            var res = [];
            var ipList = data.IPList.split(',');

            if (ipList[0] === ""){
                ipList[0] = isWeb ? window.location.host : mcConst.ServerInfo.Host;
            }

            for (var i = 0; i < ipList.length; i++){
                var tcp  = 'turn:' + ipList[i] + ':' + data.TURNPort +'?transport=tcp';
                var udp  = 'turn:' + ipList[i] + ':' + data.TURNPort +'?transport=udp';
                var stun = 'stun:' + ipList[i] + ':' + data.TURNPort;

                res.push({
                    urls       : [stun, udp, tcp],
                        //.concat(isWeb ? tcp : [])
                        // .concat(isWeb || (mcConst.ClientSettings && mcConst.ClientSettings.MediaNetworkTransportTCP) ? stun : [])
                        // .concat(isWeb || (mcConst.ClientSettings && mcConst.ClientSettings.MediaNetworkTransportTCP) ? tcp  : [])
                        // .concat(isWeb || (mcConst.ClientSettings && mcConst.ClientSettings.MediaNetworkTransportUDP) ? udp  : []),
                        // .concat(['stun:stun.l.google.com'])

                    credential : data.Credential,
                    username   : data.UserName
                });
            }

            return res;
        },

        checkRightsCollection: function(_ids, equals){
            var res = false;
            var id = null;

            for (var i = 0; i < _ids.length; i ++){
                id = parseInt(_ids[i]) - 1;

                res = equals
                    ? res && (mcConst.MyRightsSet.RightsSet[id] == '1')
                    : res || (mcConst.MyRightsSet.RightsSet[id] == '1');
            }

            return res;
        },

        MyTimer: function (_target){
            var Now = 0;
            var id = null;
            var view = _target;

            this.setView = function (newView) {
                view = newView;
            };

            this.Start = function(){
                Now = 0;

                clearInterval(id);

                id = setInterval(function(){
                    Now++;

                    if (view) {
                        if (view.innerText !== void(0)) {
                            view.innerText = serviceList.MyFormatTime(Now, true);
                        } else {
                            view.text(serviceList.MyFormatTime(Now, true));
                        }
                    }
                }, 1000);
            };

            this.Pause = function(newTime){
                clearInterval(id);

                Now = newTime || Now;
            };

            this.Stop = function(){
                if (view) {
                    if (view.innerText !== void(0)) {
                        view.innerText = serviceList.MyFormatTime(0, true);
                    } else {
                        view.text(serviceList.MyFormatTime(0, true));
                    }
                }

                clearInterval(id);

                return serviceList.MyFormatTime(Now, true);
            };
        },
        
        openLink: function(link, param){
            var params = param || "menubar=yes,location=yes,resizable=yes,scrollbars=yes,status=yes";
            window.open(link, "", params);
        },

        showImportResult: function(data, $rootScope){
            var add = [];
            var upd = [];
            var err = [];
            var res = "";

            if (data && data.Results && data.Users){
                for (var i = 0; i < data.Results.length; i++){
                    switch (data.Results[i]){
                        case '0': case 0:
                        add.push(data.Users[i]);
                        break;

                        case '1': case 1:
                        upd.push(data.Users[i]);
                        break;

                        default:
                            err.push(data.Users[i]);
                    }
                }

                if (add.length){
                    res += "<span class='blue _grName'>" + mcLang(1552) + '</span><br>';// "1552":"Добавлены пользователи:",
                    res += add.join('<br>') + '<br><br>';
                }

                if (upd.length){
                    res += "<span class='green _grName'>" + mcLang(1553) + '</span><br>';// "1553":"Обновлены пользователи:",
                    res += upd.join('<br>') + '<br><br>';
                }

                if (err.length){
                    res += "<span class='red _grName'>" + mcLang(1554) + '</span><br>';// "1554":"Не добавленные пользователи:",
                    res += err.join('<br>');
                }

                var wnd = webix.ui({
                    view: "window",
                    position: "center",
                    modal: true,
                    width: 300,
                    height: 500,
                    head: mcLang(971), // "971":"Результат:",
                    body: { rows: [
                        { template: res, borderless: true, scroll: "y" },
                        { view: "button", value: mcLang(12), click: function(){
                            this.getTopParentView().hide();
                            this.getTopParentView().destructor();
                        }} //"12" : "Закрыть",
                    ]}
                });

                $rootScope.regShowWindow(serviceList.RandomHash(4), wnd);

                wnd.show();

                if (add.length || upd.length){
                    $rootScope.$broadcast('refreshUserManagerOnNextShow', []);
                }
            }
        },

        getUserStateText: function(_state, noColor){
            var state = "";

            switch (_state){
                case mcConst.states.offline:
                    state = serviceList.Lang(1641); break; // "1641":"Не в сети",

                case mcConst.states.online:
                    state = (!noColor ? '<span class="green">' : "") + serviceList.Lang(1642) + (!noColor ? '</span>' : ""); break; // "1642":"В сети",

                case mcConst.states.webOnline:
                    state = (!noColor ? '<span class="green">' : "") + serviceList.Lang(1642) + (!noColor ? '</span>' : ""); break; // "1642":"В сети",

                case mcConst.states.away:
                    state = (!noColor ? '<span class="blue">' : "") + serviceList.Lang(1643) + (!noColor ? '</span>' : ""); break; // "1643":"Нет на месте",

                case mcConst.states.dnd:
                    state = (!noColor ? '<span class="red">' : "") + serviceList.Lang(1644) + (!noColor ? '</span>' : ""); break; // "1644":"Не беспокоить",
            }

            return state;
        },

        getColorForStatus: function(status){
            var res = "#545454";

            status = parseInt(status);

            switch (status){
                case mcConst.states.offline: res = '#929292';  break; // "29" : "Не в сети",

                case mcConst.states.online: res = '#2bca2e'; break; // "30" : "В сети",

                case mcConst.states.webOnline: res = '#2bca2e'; break; // "30" : "В сети",

                case mcConst.states.away: res = 'blue'; break; // "31" : "Нет на месте",

                case mcConst.states.dnd: res = 'red'; break; // "32" : "Не беспокоить",
            }

            return res;
        },

        smartReplace: function (source, options) {
            for (var item in options){
                source = source.replace(new RegExp('{' + item + '}', 'g'), options[item]);
            }

            return source;
        },

        getIPs: function (callback){
            function handleCandidate(candidate){
                //match just the IP address
                var ip_regex = /([0-9]{1,3}(\.[0-9]{1,3}){3}|[a-f0-9]{1,4}(:[a-f0-9]{1,4}){7})/
                var ip_addr = ip_regex.exec(candidate)[1];

                //remove duplicates
                if(ip_dups[ip_addr] === undefined)
                    callback(ip_addr);

                ip_dups[ip_addr] = true;
            }

            var ip_dups = {};

            //compatibility for firefox and chrome
            var RTCPeerConnection = window.RTCPeerConnection
                || window.mozRTCPeerConnection
                || window.webkitRTCPeerConnection;
            var useWebKit = !!window.webkitRTCPeerConnection;

            //bypass naive webrtc blocking using an iframe
            if(!RTCPeerConnection){
                //NOTE: you need to have an iframe in the page right above the script tag
                //
                //<iframe id="iframe" sandbox="allow-same-origin" style="display: none"></iframe>
                //<script>...getIPs called in here...
                //
                var win = iframe.contentWindow;
                RTCPeerConnection = win.RTCPeerConnection
                    || win.mozRTCPeerConnection
                    || win.webkitRTCPeerConnection;
                useWebKit = !!win.webkitRTCPeerConnection;
            }

            //minimal requirements for data connection
            var mediaConstraints = {
                optional: [{RtpDataChannels: true}]
            };

            var servers = {iceServers: [{urls: "stun:stun.services.mozilla.com"}]};

            //construct a new RTCPeerConnection
            var pc = new RTCPeerConnection(servers, mediaConstraints);

            //listen for candidate events
            pc.onicecandidate = function(ice){

                //skip non-candidate events
                if(ice.candidate)
                    handleCandidate(ice.candidate.candidate);
            };

            //create a bogus data channel
            pc.createDataChannel("");

            //create an offer sdp
            pc.createOffer(function(result){

                //trigger the stun server request
                pc.setLocalDescription(result, function(){}, function(){});

            }, function(){});

            //wait for a while to let everything done
            setTimeout(function(){
                //read candidate info from local description
                var lines = pc.localDescription.sdp.split('\n');

                lines.forEach(function(line){
                    if(line.indexOf('a=candidate:') === 0)
                        handleCandidate(line);
                });
            }, 1000);

            //Test: Print the IP addresses into the console
            //getIPs(function(ip){console.log(ip);});
        },

        treeWalker: function(treeArray, deepIn, cb){
            if (serviceList.inArray(treeArray)){
                for (var i = 0; i < treeArray.length; i++){
                    if (cb) cb(treeArray[i]);

                    if (treeArray[i].hasOwnProperty(deepIn)) serviceList.treeWalker(treeArray[i][deepIn], deepIn, cb);
                }
            }
        },

        updateStatuses: function (stList, dataList){
            function setState(_user) {
                if (_user.hasOwnProperty("UIN")){
                    if (stList.Statuses && stList.Users) {
                        _user.onlineState = stList.Statuses[stList.Users.indexOf(_user.UIN.toString())];
                    } else {
                        // _user.State = stList[_user.UIN.toString()] !== undefined ? stList[_user.UIN.toString()] : mcConst.states.offline;
                        _user.onlineState = stList[_user.UIN] !== undefined ? stList[_user.UIN] : mcConst.states.offline;
                    }

                    dataList.updateItem && dataList.updateItem(_user.id, _user);
                }
            }

            if (stList && dataList){
                if (serviceList.isArray(dataList)){
                    dataList.forEach(setState);
                } else {
                    dataList.each(setState);
                }
            }
        },

        getCaretPosition: function  (ctrl) {
            var CaretPos = 0;

            if (document.selection) {
                var Sel = document.selection.createRange();

                Sel.moveStart('character', - ctrl.value.length);

                CaretPos = Sel.text.length;
            } else

            // Fucking firefox support
            if (ctrl.selectionStart || ctrl.selectionStart == '0'){
                CaretPos = ctrl.selectionStart;
            }

            return CaretPos;

        },

        setCaretPosition: function (ctrl, pos, selectCount){
            pos = parseInt(pos);
            
            if(ctrl.setSelectionRange) {
                ctrl.setSelectionRange(pos, pos + (selectCount || 0));
            } else
            if (ctrl.createTextRange) {
                var range = ctrl.createTextRange();

                range.collapse(true);
                range.moveEnd('character', pos + (selectCount || 0));
                range.moveStart('character', pos);
                range.select();
            }
        },

        goToHelp : function (_path, go) {
            var path = (_path || window.location.hash).replace(/^#\/|^\/|\/$/g, "");
            var localPath =  serviceList.isIE()
                ? window.location.protocol + "//" + window.location.host
                : window.location.origin;

            if (_path && _path.toLowerCase().indexOf("http") === 0){
                window.open(_path, '', '');
            } else
            if (mcConst._CMD_.accordance.hasOwnProperty(path) && mcConst._CMD_.accordance[path] !== ""){
                window.open(localPath + serviceList.myReplaceFormated(mcConst._CMD_.accordance[path], {lang: mcConst.Lang === "uk" ? "ua" : mcConst.Lang}), '', '');
                // window.open(localPath + serviceList.myReplaceFormated(mcConst._CMD_.accordance[path], {lang: mcConst.Lang === "en" ? "_en" : ""}), '', '');
            } else
            if (go){
                window.open(localPath + "/" + path, '', '');
            }
        },

        getHelpLink : function (_path, text, _class, _id) {
            var path = (_path || window.location.hash).replace(/^#\/|^\/|\/$/g, "");
            var localPath =  serviceList.isIE()
                ? window.location.protocol + "//" + window.location.host
                : window.location.origin;
            var res = "";

            if (_path){
                res = serviceList.myReplaceFormated(
                    "<a href='#{link}' target='_blank' class='#{class}' id='#{id}'>#{text}</a>",
                    {
                        text : text || "Link",
                        link : (_path.toLowerCase().indexOf("http") === 0 ? _path : localPath + _path),
                        class: _class || "",
                        id   : _id || ""
                    }
                );
            } else
            if (mcConst._CMD_.accordance.hasOwnProperty(path) && mcConst._CMD_.accordance[path] !== ""){
                res = serviceList.myReplaceFormated(
                    "<a href='${link}' target='_blank' class='${class}' id='${id}'>${text}</a>",
                    {
                        text : text || "Link",
                        link : localPath + serviceList.myReplaceFormated(mcConst._CMD_.accordance[path], {lang: mcConst.Lang === "uk" ? "ua" : mcConst.Lang}),
                        class: _class || "",
                        id   : _id || ""
                    }
                );
            }

            return res;
        },

        HotkeyManager: function(debug){
            var keyList     = {};
            var presetList  = {};
            var maxKeyLevel = 0;
            var firedKeys   = {};
            var sourceMap   = {}; // key: [marker1, marker2, ...]
            var sourceMarker= "hotKeyMarker" + serviceList.RandomHash(5);
            var disableAll  = false;
            var removeList  = [];
            var removeTimer = null;

            function convertToKeyString(obj, code){
                return  code
                    ? (obj.ctrlKey ? "ctrl+" : "") +
                      (obj.shiftKey? "shift+": "") +
                      (obj.altKey  ? "alt+"  : "") +
                      code.toString()
                    : null;
            }

            function findByKeyCode(name, key) {
                var res = null;

                presetList[name].source.forEach(function (preset) {
                    if (convertToKeyString(preset, preset.key) === key.toString()){
                        res = preset;
                    }
                });

                return res;
            }

            function registerKeys(name, preset){
                var source = preset.source;

                for (var i = 0; i < source.length; i++){
                    var key = convertToKeyString(source[i], source[i].key);

                    if (!keyList.hasOwnProperty(key)){
                        keyList[key] = {};
                    }

                    // if (debug) {
                    //     console.log(
                    //       `+ Registered key: ${key}`+
                    //       `  Preset        : ${name}` +
                    //       `  maxKeyLevel   : ${maxKeyLevel}`);
                    // }

                    keyList[key][name] = source[i].func;

                    if (sourceMap[key]){
                        sourceMap[key].push(preset.marker);
                    } else {
                        sourceMap[key] = [preset.marker];
                    }
                }

                if (preset.target && !preset.ID){
                    preset.ID = webix.event(preset.target, "keydown", function(e){
                        if (disableAll) {
                            e.preventDefault();
                            
                            return false;
                        }

                        var keyCode     = convertToKeyString(e, e.keyCode);
                        var skipDefault = false;
                        var where       = this;

                        if (keyList.hasOwnProperty(keyCode) &&
                            where.hasOwnProperty(sourceMarker) &&
                            sourceMap.hasOwnProperty(keyCode) &&
                            sourceMap[keyCode].indexOf(where[sourceMarker]) !== -1
                        ) {
                            if (!firedKeys.hasOwnProperty(keyCode)) {
                                if (debug) {
                                    console.log('Fired key: '+ keyCode);
                                    // console.log(e);
                                }

                                firedKeys[keyCode] = 'fired';

                                for (var i in keyList[keyCode]){
                                    if (where[sourceMarker] === presetList[i].marker){
                                        if (presetList[i].level >= maxKeyLevel){
                                            var _preset = findByKeyCode(i, keyCode); // находим совпадающий индекс сочетания клавиш

                                            if (!_preset._locked){                            // если у пресета нет лока - выполняем
                                                if (debug) {
                                                    console.log('Run func: '+ _preset.func.name);
                                                }

                                                skipDefault = !keyList[keyCode][i](e, where); // запуск событие
                                            } else {
                                                if (debug) {
                                                    console.log('! Func  locked: '+ _preset.func.name);
                                                    console.log('! locked value: '+ _preset._locked);
                                                }
                                            }
                                        } else {
                                            var source = presetList[i].source;

                                            for (var j in source){
                                                if (convertToKeyString(source[j], source[j].key) === keyCode){
                                                    skipDefault = source[j].skipDefault;

                                                    break;
                                                }
                                            }
                                        }
                                    }
                                }

                                (function (key) {
                                    setTimeout(function () {
                                        delete firedKeys[key];
                                    }, 10);
                                })(keyCode);
                            }

                            if (skipDefault){
                                e.preventDefault();

                                return false;
                            }
                        }
                    });
                }
            }

            function removeKeys(_name){
                function _doRemove(name) {
                    for (var keyCode in keyList){
                        if (keyList[keyCode].hasOwnProperty(name)){
                            if (presetList[name].level === maxKeyLevel && maxKeyLevel > 0){
                                var entries = 0;
                                var prevKeyLevel = 0;

                                Object.keys(presetList).forEach(function (preset) {
                                    var curLevel = presetList[preset].level;

                                    if (curLevel === maxKeyLevel){
                                        entries ++;
                                    }

                                    if (curLevel > prevKeyLevel && curLevel < maxKeyLevel){
                                        prevKeyLevel = curLevel;
                                    }
                                });

                                if (entries === 1){
                                    maxKeyLevel = prevKeyLevel;
                                }
                            }

                            if (debug) {
                                console.log('- Removing key: ' + keyCode);
                                console.log('- Preset: ' + name);
                                console.log('- maxKeyLevel: ' + maxKeyLevel);
                            }

                            removeLock(keyCode, presetList[name].key);

                            sourceMap[keyCode].splice(sourceMap[keyCode].indexOf(presetList[name].marker), 1);

                            if (sourceMap[keyCode].length === 0){
                                delete sourceMap[keyCode];
                            }

                            delete keyList[keyCode][name];
                        }
                    }

                    if (presetList[name]){
                        webix.eventRemove(presetList[name].ID);

                        delete presetList[name];
                    }

                    if (removeList.length){
                        return _doRemove(removeList.shift());
                    } else {
                        clearTimeout(removeTimer);
                        removeTimer = null;
                    }
                }
                
                if (_name) removeList.push(_name);

                if (!removeTimer){
                    removeTimer = setTimeout(function (){
                        _doRemove(removeList.shift());
                    }, 10);
                }
            }

            function lockAction(key, except) {
                for (var i in presetList){
                    presetList[i].source.forEach(function (preset) {
                        if (i !== except && key.toString() === convertToKeyString(preset, preset.key)){
                            if (preset._locked){
                                preset._locked ++;
                            } else {
                                preset._locked = 1;
                            }

                            if (debug) {
                                console.log('Add lock key: '+ key + ', value: ' + preset._locked);
                                console.log(presetList[i]);
                            }
                        } else {
                            preset._locked = 0;
                        }
                    });
                }
            }

            function removeLock(key) {
                for (var i in presetList){
                    presetList[i].source.forEach(function (preset) {
                        if (key.toString() === convertToKeyString(preset, preset.key) && preset.hasOwnProperty("_locked") && preset._locked > 0){
                            preset._locked --;

                            if (debug) {
                                console.log('remove lock key: '+ key + ', value: ' + preset._locked);
                                console.log(preset);
                            }
                        }
                    });
                }
            }

            this.disable = function (){
                disableAll = true;
            };

            this.enable = function (){
                disableAll = false;
            };

            this.debug = function (state){
                debug = !!state;

                console.log(`HotkeyManager debugger is ${!!state ? 'on': 'off'}`);
            };

            this.showPresetList = function () {
                console.log("showPresetList");
                
                Object.keys(presetList).forEach(function (name) {
                    console.log(presetList[name]);
                    // console.log(
                    //     `========================` +
                    //     `name  : ${name}` +
                    //     `preset: ${mcService.ObjToString(presetList[name].preset)}` +
                    //     `target: ${presetList[name].target.tagName || presetList[name].target.nodeName}` +
                    //     `ID    : ${presetList[name].ID}`
                    // );
                });
            };

            this.clearPresetLevel = function (level) {
                debug && console.log("clearPresetLevel: " + level);

                Object.keys(presetList).forEach(function (name) {
                    if (presetList[name].level >= level){
                        removeKeys(name);
                    }
                })
            }

            this.addPreset = function(name, preset, target, disablePrevious){
                if (!presetList.hasOwnProperty(name)){
                    var marker = serviceList.RandomHash(10);

                    if (target.hasOwnProperty(sourceMarker)){
                        marker = target[sourceMarker];
                    } else {
                        target[sourceMarker] = marker;
                    }

                    presetList[name] = {
                        name  : name,
                        source: preset,
                        target: target,
                        marker: marker,
                        ID    : null
                    };

                    if (disablePrevious) {
                        maxKeyLevel ++;
                    }

                    preset.forEach(function (item) {
                        if (item.lockPrev){
                            lockAction(convertToKeyString(item, item.key), name);
                        }
                    });

                    presetList[name].level = maxKeyLevel;

                    if (debug) {
                        console.log("addPreset:");
                        console.log(presetList[name]);
                        // console.log(
                        //     `+++++\n` +
                        //     `addPreset: ${name}\n` +
                        //     `preset   : ${mcService.ObjToString(preset)}\n` +
                        //     `target   : ${target.tagName || target.nodeName}\n` +
                        //     `level    : ${maxKeyLevel}\n` +
                        //     `disablePrevious: ${!!disablePrevious}\n`
                        // );
                    }

                    registerKeys(name, presetList[name]);
                } else {
                    // console.warn("Preset \"" + name + "\" is present");

                    return false;
                }
            };

            this.removePreset = function(name){
                if (presetList.hasOwnProperty(name)){
                    removeKeys(name);
                } else {
                    return false;
                }
            };
        },

        utcTime: function(msg, decode, format) {
            var dt = null;

            if (mcService.isDate(msg)){
                dt = msg;
            } else
            if (serviceList.isNumber(msg)) {
                dt = new Date(msg);
            } else {
                dt = msg.dtUTC || msg.dt || msg;
            }

            return dt
                ? serviceList.formatDate(new Date(
                    (new (Function.prototype.bind.apply(
                        Date,
                        [null].concat(serviceList.formatDate(dt, "yyyy.mm.dd.hh.nn.ss.zzz").split('.').map(function (val, idx) {
                            return idx === 1 ? val - 1 : val;
                        }))
                    ))).getTime() + (new Date()).getTimezoneOffset() * 1000 * 60 * (!decode ? -1 : 1)
                ), format || "dd.mm.yyyy.hh.nn.ss.zzz")
                : ""
            ;
        },

        reloadOrChangeProtocol: function () {
            if (mcConst.changeProtocol && mcConst.changeProtocol !== location.protocol) {
                location.protocol = mcConst.changeProtocol;

                window.open(location.href);
            } else {
                location.search = "";
                location.reload(true);
            }
        },

        myReplaceFormated : function(source, items){
            if (serviceList.isObject(items) && serviceList.isString(source)){
                for (var i in items) {
                    source = source.replace(new RegExp("#{" + i + "}", "g"), items[i]);
                }
            }

            return source;
        },

        myReplaceUni : function(source, items){
            if (serviceList.isObject(items) && serviceList.isString(source)){
                for (var i in items){
                    source = source.replace(new RegExp(i, "g"), items[i]);
                }
            }

            return source;
        },

        createKanbanLink: function (projectID, taskID, commentID) {
            return serviceList.getLocalHostPath(location.protocol !== 'file:') +
                "/" + mcConst.pathAliases.AliasKanban + "/#project=" + projectID + (taskID ? "&task=" + taskID + (commentID ? "&comment=" + commentID : "") : "");
        },

        createForumLink: function (path){
            return serviceList.getLocalHostPath(location.protocol !== 'file:') + "/" + mcConst.pathAliases.AliasForum + "/#/" + path;
        },

        getCommonFilesPath: function (isWebClient) {
            return this.myReplaceFormated("ftp://#{user}:#{pwd}@#{host}:#{port}/", {
                host : isWebClient ? window.location.host : mcConst.ServerInfo.Host,
                user : mcConst.FTP.CommonUser,
                pwd  : mcConst.FTP.CommonPWD,
                port : mcConst.UserInfo.PortFTP
            });
        },

        SHA1: function () {
            var sha1 = new Rusha();
            sha1.resetState();

            return sha1;
        },

        convertToEntities : function(tstr) {
            function dec2hex(i){
                var result = "0000";

                if      (i >= 0    && i <= 15)    { result = "000" + i.toString(16); }
                else if (i >= 16   && i <= 255)   { result = "00"  + i.toString(16); }
                else if (i >= 256  && i <= 4095)  { result = "0"   + i.toString(16); }
                else if (i >= 4096 && i <= 65535) { result =         i.toString(16); }

                return result
            }
            var bstr = "";

            if (tstr){
                for(var i = 0; i < tstr.length; ++i)
                {
                    if(tstr.charCodeAt(i) > 127)
                    {
                        bstr += "\\u" + dec2hex(tstr.charCodeAt(i));
                    } else {
                        bstr += tstr.charAt(i);
                    }
                }
            } else {
                throw new Error('convertToEntities: incoming data is empty or undefined: ' + tstr);
                //console.error('convertToEntities: incoming data is empty or undefined', true);
            }

            return bstr;
        },

        extractFileName : function(Path){
            var count = 0;

            if (Path){
                for (var i = Path.length - 1; i >= 0; i--){
                    count ++;

                    if ((Path[i] === '\\') || (Path[i] === '/')) {
                        return Path.slice(i + 1, i + count);
                    }
                }
            }

            return Path;
        },

        extractFileExtension : function(Path){
            var path = (Path || "").split('.');

            return path[path.length - 1];
        },

        extractFilePath : function(Path){
            for (var i = Path.length - 1; i >= 0; i--){
                if ((Path[i] === '\\') || (Path[i] === '/')) {
                    return Path.slice(0, i);
                }
            }

            return Path;
        },

        getLocalHostPath: function (isWebClient) {
            var res = "";

            if (isWebClient) {
                res = serviceList.isIE() ? window.location.protocol + "//" + window.location.host : window.location.origin
            } else {
                var host = mcConst.UserInfo.useAltAddr ? mcConst.ServerInfo.AltHost : mcConst.ServerInfo.Host;
                var port = mcConst.UserInfo.PortNode;

                res = (mcConst.UserInfo.HTTPS ? "https://" : "http://") + (mcConst.ServerInfo.IPv6 ? "[" + host + "]" : host) +
                      (mcConst.UserInfo.HTTPS ? (port != 443 ? ':' + port : '') : (port != 80  ? ':' + port : ''))
            }

            return res;
        },

        createHostPath: function (_host, _port, noProtocol) {
            var port  = _port ? _port : mcConst.UserInfo.PortNode;
            var host  = _host ? _host : mcConst.ServerInfo.Host;
            var https = window.location.protocol !== 'file:' ? window.location.protocol === "https:" : mcConst.UserInfo.HTTPS;
            
            return (
                noProtocol
                    ? ""
                    : (https ? "https://" : "http://")
                ) +
                (host.indexOf(":") === -1 // no port
                    ? host + (https
                            ? (port != 443 ? ':' + port : '')
                            : (port != 80  ? ':' + port : '')
                      )
                    : host
                );
        },

        formatFileSize: function(size, round){
            var index = 0;

            while (size > 1024){
                index ++;

                size = size / 1024;
            }

            return (round ? Math.round(Math.round(size * 100)/100) : Math.round(size * 100)/100) + " " + ["B","KB","MB","GB","TB","PB","EB"][index];
        },

        fileTimeStamp: function (dd) {
            // var currentTimeZoneOffsetInMS = (new Date()).getTimezoneOffset() * 60 * 1000;
            // var now  = new Date((new (Function.prototype.bind.apply(
            //     Date,
            //     [null].concat(serviceList.formatDate(dd ? new Date(dd) : new Date(), "yyyy.mm.dd.hh.nn.ss.zzz").split('.').map(function (val, idx) {
            //         return idx === 1 ? val - 1 : val;
            //     }))
            // ))).getTime() - currentTimeZoneOffsetInMS);
            // var then = new Date(
            //     now.getFullYear(),
            //     now.getMonth(),
            //     now.getDate(),
            //     0,0,0
            // );
            // var diff = ((now.getTime() - then.getTime()) || (24 * 60 * 60 * 1000));// + currentTimeZoneOffsetInMS;
            if (typeof require === 'function') {
                window.moment = require('moment');
            }

            var now   = serviceList.utcTime(dd ? new Date(dd) : new Date(), true).split('.');
            // 0  1  2    3  4  5  6
            // dd.mm.yyyy.hh.nn.ss.zzz
            var diff  = parseInt(now[6]) + parseInt(now[5])*1000 + parseInt(now[4])*1000*60 + parseInt(now[3])*1000*60*60;
            var diff2 = moment(new Date(now[2], parseInt(now[1]) - 1, now[0], now[3], now[4], now[5])).startOf('day').diff(moment({year:"1", month: "0", day: "1"}).startOf('day'), 'days') + 1;
                                                                                                                           
            return diff + "." + diff2;
        },

        getSelectedText: function (element){
            var res = {
                selectionStart : element.selectionStart,
                selectionEnd   : element.selectionEnd
            };

            // nothing is selected
            if (res.selectionStart !== res.selectionEnd) {
                var string = element.value;

                res.prefix  = string.substring(0, res.selectionStart);
                res.infix   = string.substring(res.selectionStart, res.selectionEnd);
                res.postfix = string.substring(res.selectionEnd);
            }

            // element.value = prefix + 'REPLACED TEXT' + postfix;

            return  res;
        },

        setSelectionText: function(inputBox, start, end){
            if (start > end) {
                start = end;
            }

            if ("selectionStart" in inputBox) { //gecko
                inputBox.setSelectionRange(start, end);
            } else {
                var r = inputBox.createTextRange();
                r.collapse(true);
                r.moveStart('character',start);
                r.moveEnd('character',end - start);
                r.select();
            }
        },
        
        convertArrayToObj: function (arr, item) {
            var res = {};

            if (item && serviceList.isString(item) && serviceList.isArray(arr)){
                arr.forEach(function (val) {
                    res[val[item]] = val;
                })
            } else {
                console.error('convertArrayToObj: is not Array or Item is not string!');
            }

            return res;
        },

        convertArrayObjToObj: function(arr, keyField){
            var res = {};

            for (var i = 0; i < arr.length; i++){
                if (arr[i].hasOwnProperty(keyField)){
                    res[arr[i][keyField]] = arr[i];
                }
            }

            return res;
        },

        getAvatar: function (sex) {
            var avatar = "";

            switch (sex){
                case 0: // alien
                    avatar = serviceList.insertIco("fa-user taskAvatar"); break;

                case 1: // male
                    avatar = serviceList.insertIco("fa-male taskAvatar"); break;

                case 2: // female
                    avatar = serviceList.insertIco("fa-female taskAvatar"); break;
            }

            return avatar;
        },

        getPriority: function (priority) {
            var res = "";

            switch (parseInt(priority)){
                case mcConst.priority.LOW      : res = mcLang(61); break;
                case mcConst.priority.MEDIUM   : res = mcLang(62); break;
                case mcConst.priority.HIGH     : res = mcLang(63); break;
                case mcConst.priority.IMPORTANT: res = mcLang(64); break;
                case mcConst.priority.URGENT   : res = mcLang(65); break;
                case mcConst.priority.CRITICAL : res = mcLang(66); break;
            }

            return res;
        },

        openKanbanLinkIfCan : function (e, $rootScope) {
            var res = false;

            if (e.target.nodeName === "A" && (
                    (e.target.origin + e.target.pathname == window.location.origin + window.location.pathname) ||
                    ((serviceList.isIE() && e.target.protocol + "//" + e.target.host + e.target.pathname) == (window.location.protocol + "//" + window.location.host + window.location.pathname))
                )){
                setTimeout(function () {
                    $rootScope.$broadcast('openKanbanURI', [e.target.href]);
                }, 10);

                e.preventDefault();

                res = true;
            }

            return res;
        },

        replaceKanbanLink: function (link, text) {
            var res = '<a href="' + link + '" title="' + link + '" ' + (link.indexOf('mailto') == -1 ? 'target="_blank"' : '') + ' class="internalLink">' + (text || link) + '</a>';

            if ((new RegExp(serviceList.isIE()
                    ? window.location.protocol + "//" + window.location.host + window.location.pathname
                    : window.location.origin + window.location.pathname
                )).test(link)){

                var parse = purl(link);

                var projectID = parse.fparam("project");
                var taskID    = parse.fparam("task");
                var commentID = parse.fparam("comment");

                res = '<a href="' + link + '" title="' + link + '" ' + (link.indexOf('mailto') == -1 ? 'target="_blank"' : '') + ' class="internalLink">';

                if (commentID){
                    res += "COMMENT #" + commentID;
                } else
                if (taskID){
                    res += "TASK #" + taskID;
                } else
                if (projectID){
                    res += "PROJECT #" + projectID;
                }

                res += '</a>';
            }

            return res;
        },

        getByCssClass: function(classList, _node){
            var res = null;
            var node = _node || document;

            if (document.getElementsByClassName) {
                res = node.getElementsByClassName(classList)
            } else {
                var list = node.getElementsByTagName('*');
                var length = list.length;
                var classArray = classList.split(/\s+/);
                var classes = classArray.length;
                var result = [];

                for (var i = 0; i < length; i++) {
                    for (var j = 0; j < classes; j++) {
                        if (list[i].className.search('\\b' + classArray[j] + '\\b') !== -1) {
                            result.push(list[i]);

                            break
                        }
                    }
                }

                res = result;
            }

            return res;
        },

        disableContextMenu : function(_view, exception){
            webix.event(_view.$view, "contextmenu", function(e){
                function skipItems(items, where) {
                    var flag = true;

                    if (items && items.length){
                        items.forEach(function (item) {
                            if (where.length && where.indexOf(item) !== -1){
                                flag = false;
                            }
                        });
                    }

                    return flag;
                }

                if ((
                        e.target.nodeName !== "INPUT" &&
                        e.target.nodeName !== "TEXTAREA" &&
                        e.target.parentNode && e.target.parentNode.parentNode &&
                        skipItems(exception, e.target.className.split(" ").concat(e.target.parentNode.className.split(" ")).concat(e.target.parentNode.parentNode.className.split(" ")))
                    ) ||
                    (e.target.type === "button")) {

                    e.preventDefault();
                }
            });
            // webix.event(_view.$view, "contextmenu", function(e){
            //     if ((e.target.nodeName != "INPUT") && (e.target.nodeName != "TEXTAREA") || (e.target.type == "button")) {
            //         e.preventDefault();
            //     }
            // });
        },

        setStatusForUser: function(_user, state){
            var users = serviceList.getElementByClass(mcConst.storageOpts.STATUSICO + _user);

            for (var i = 0; i < users.length; i++){
                users[i].style.color = serviceList.getColorForStatus(state);

                if (users[i].className.indexOf("fa-user smaller") === -1){
                    users[i].style.backgroundColor = serviceList.getColorForStatus(state);
                }
            }
        },

        Base64 : {
            // private property
            _keyStr : "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=",

            // public method for encoding
            encode : function (input) {
                var output = "";
                var chr1, chr2, chr3, enc1, enc2, enc3, enc4;
                var i = 0;

                input = serviceList.Base64._utf8_encode(input);

                while (i < input.length) {
                    chr1 = input.charCodeAt(i++);
                    chr2 = input.charCodeAt(i++);
                    chr3 = input.charCodeAt(i++);

                    enc1 = chr1 >> 2;
                    enc2 = ((chr1 & 3) << 4) | (chr2 >> 4);
                    enc3 = ((chr2 & 15) << 2) | (chr3 >> 6);
                    enc4 = chr3 & 63;

                    if (isNaN(chr2)) {
                        enc3 = enc4 = 64;
                    } else if (isNaN(chr3)) {
                        enc4 = 64;
                    }

                    output = output +
                        this._keyStr.charAt(enc1) + this._keyStr.charAt(enc2) +
                        this._keyStr.charAt(enc3) + this._keyStr.charAt(enc4);

                }

                return output;
            },

            // public method for decoding
            decode : function (input) {
                var output = "";
                var chr1, chr2, chr3;
                var enc1, enc2, enc3, enc4;
                var i = 0;

                input = input.replace(/[^A-Za-z0-9\+\/\=]/g, "");

                while (i < input.length) {

                    enc1 = serviceList.Base64._keyStr.indexOf(input.charAt(i++));
                    enc2 = serviceList.Base64._keyStr.indexOf(input.charAt(i++));
                    enc3 = serviceList.Base64._keyStr.indexOf(input.charAt(i++));
                    enc4 = serviceList.Base64._keyStr.indexOf(input.charAt(i++));

                    chr1 = (enc1 << 2) | (enc2 >> 4);
                    chr2 = ((enc2 & 15) << 4) | (enc3 >> 2);
                    chr3 = ((enc3 & 3) << 6) | enc4;

                    output = output + String.fromCharCode(chr1);

                    if (enc3 !== 64) {
                        output = output + String.fromCharCode(chr2);
                    }

                    if (enc4 !== 64) {
                        output = output + String.fromCharCode(chr3);
                    }

                }

                output = serviceList.Base64._utf8_decode(output);

                return output;

            },

            // private method for UTF-8 encoding
            _utf8_encode : function (string) {
                string = string.replace(/\r\n/g,"\n");
                var utftext = "";

                for (var n = 0; n < string.length; n++) {

                    var c = string.charCodeAt(n);

                    if (c < 128) {
                        utftext += String.fromCharCode(c);
                    } else
                    if((c > 127) && (c < 2048)) {
                        utftext += String.fromCharCode((c >> 6) | 192);
                        utftext += String.fromCharCode((c & 63) | 128);
                    } else {
                        utftext += String.fromCharCode((c >> 12) | 224);
                        utftext += String.fromCharCode(((c >> 6) & 63) | 128);
                        utftext += String.fromCharCode((c & 63) | 128);
                    }

                }

                return utftext;
            },

            // private method for UTF-8 decoding
            _utf8_decode : function (utftext) {
                var string = "";
                var i  = 0;
                var c  = 0;
                var c1 = 0;
                var c2 = 0;
                var c3 = 0;

                while ( i < utftext.length ) {

                    c = utftext.charCodeAt(i);

                    if (c < 128) {
                        string += String.fromCharCode(c);
                        i++;
                    } else
                    if((c > 191) && (c < 224)) {
                        c2 = utftext.charCodeAt(i+1);
                        string += String.fromCharCode(((c & 31) << 6) | (c2 & 63));
                        i += 2;
                    } else {
                        c2 = utftext.charCodeAt(i+1);
                        c3 = utftext.charCodeAt(i+2);
                        string += String.fromCharCode(((c & 15) << 12) | ((c2 & 63) << 6) | (c3 & 63));
                        i += 3;
                    }

                }

                return string;
            }
        },

        isOdd : function (num) { return num % 2;},

        replaceMcComponents : function (text, isWeb, $rs, nick) {
            var items = text.split(mcConst.terminator2);
            var elem  = "";
            var smileFile = "";

            items.forEach(function (item, idx) {
                if (serviceList.isOdd(idx)){
                    elem = item.split(mcConst.terminator3);

                    switch (item[0]) {
                        case ")":
                            try {
                                elem[0] = elem[0].replace(")", "");

                                if (mcConst.SmileysSet[elem[0]]){
                                    smileFile = mcConst.SmileysSet[elem[0]][elem[1]];
                                }

                                if (smileFile && (isWeb || mcConst.ClientSettings.SysEventsDisableEmotions == '0')){
                                    items[idx] = serviceList.myReplaceFormated(
                                        "<img src='#{path}/#{smile}'>",
                                        {
                                            path : MC_RESOURCE.TextSource.smileys + mcConst.SmileysSet[elem[0]].name,
                                            smile: smileFile
                                        }
                                    );
                                } else {
                                    items[idx] = elem[1];
                                }
                            } catch (e) {
                                items[idx] = ":)";
                            }
                        break;

                        case "F":
                            var style = elem[0].split("#");
                            style = style[style.length - 1];

                            items[idx] = elem[1];

                            for (var i = 0; i < style.length; i++){
                                switch (style[i]) {
                                    case "B": items[idx] = "<b>" + items[idx] + "</b>"; break;
                                    case "I": items[idx] = "<i>" + items[idx] + "</i>"; break;
                                    case "U": items[idx] = "<u>" + items[idx] + "</u>"; break;
                                    case "S": items[idx] = "<strike>" + items[idx] + "</strike>"; break;
                                }
                            }
                        break;
                            
                        case ">":
                            var uin = elem[0].replace(">", "");

                            var name = elem[1];

                            if (name.indexOf('noname (') === 0) {
                                name = nick ? nick : $rs.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [uin]).DisplayName;
                            }

                            items[idx] = "<span uin='" + uin +"' class='brown bolder'>@" + name + "</span>";
                        break;

                        case "A":
                            var link = elem[0].replace("A", "");

                            if (link.indexOf("\\") === 0) {
                                link = link.replace("\\\\", "local://").replace(/\\/g, "/");
                            }

                            items[idx] = link + mcConst.myChatLinkTitle + elem[1];
                        break;

                        case "f": // public FTP
                            items[idx] = mcConst.ftpPublic;
                        break;

                        case "m": // personal FTP                 // uin
                            items[idx] = mcConst.ftpPersonal + elem[0].replace("m", "");
                        break;
                    }
                }
            });
            
            return items.join("");
        },

        numberList: function (length, forwardZero) {
            var strLength = length.toString().length;
            var res = (new Array(length || 1)).join('.').split('.');

            res = res.map(function (itm, idx) {
                var st = idx.toString();

                return forwardZero ? (new Array(strLength + 1 - st.length)).join('0') + st : st;
            });

            return res;
        },

        forvardZero: function (st, strLength, symbol) {
            st = st.toString();
            
            return (new Array(strLength + 1 - st.length)).join(symbol || '0') + st;
        },

        toHex: function (numb, hexLen){
            var str = "";

            numb = parseInt(numb, 10);

            while (numb > 0){
                str  = hexCodes[numb % 16] + str;
                numb = Math.floor(numb / 16);
            }

            while (str.length < hexLen){
                str = "0" + str;
            }

            return str;
        },

        hexToDec: function(hex){
            return parseInt(hex, 16);
        },

        FadeControl: function Fade(elem) {
            var el   = elem;
            var stop = null;

            this.in = function () {
                stop = "out";
    
                var last = new Date();
                var tick = function() {
                    if (stop === "in") {
                        stop = null;
                    } else {
                        el.style.opacity = + el.style.opacity + (new Date() - last) / 400;

                        last = new Date();

                        if (el.style.opacity <= 1) {
                            (window.requestAnimationFrame && requestAnimationFrame(tick)) || setTimeout(tick, 16);
                        } else {
                            stop = null;
                        }
                    }
                };

                tick();
            };

            this.out = function () {
                stop = "in";

                var last = new Date();
                var tick = function() {
                    if (stop === "out"){
                        stop = null;
                    } else {
                        el.style.opacity = + el.style.opacity - (new Date() - last) / 400;

                        last = new Date();

                        if (el.style.opacity > 0) {
                            (window.requestAnimationFrame && requestAnimationFrame(tick)) || setTimeout(tick, 16);
                        } else {
                            stop = null;
                        }
                    }
                };

                tick();
            };
        },

        Capitalize: function (str) {
            return str[0].toUpperCase() + str.substring(1).toLowerCase();
        },

        ObjToString : function(_obj){
            var res = "";

            if (serviceList.isString(_obj) || serviceList.isNumber(_obj)){
                res = _obj.toString() + "";
            } else {
                try {
                    res = JSON.stringify(_obj);
                } catch (e){
                    res = _obj.toString() + "";
                }
            }

            return res;
        },

        typeFlag: function (displayFlag, lookField) {
            var lng = $$(displayFlag);
            var lastChar = $$(lookField).getValue();
            lastChar = lastChar[lastChar.length - 1];

            if (lastChar){
                if (lastChar.match(/[a-zA-Z]/)) {
                    lng.define('template', 'EN');
                    lng.refresh();
                } else
                if (lastChar.match(/[а-яА-Я]/)){
                    lng.define('template', 'RU');
                    lng.refresh();
                } else
                if (lastChar.match(/[їЇіІєЄ]/)){
                    lng.define('template', 'UA');
                    lng.refresh();
                }
            }
        },

        buildForumStructure: function (struct, data, ids, structCash, exeptTopicks) {
            var newIds  = [];
            var newData = [];
            var idx     = 0;
            var item    = null;

            while (idx < data.length){
                item = data[idx];

                if (item){
                    if (item.Type === mcConst.msgTypes.topic && exeptTopicks){
                        data.splice(idx, 1);
                    } else

                    if (serviceList.inArrayNoStrict(item.ParentID, ids) !== -1){
                        item.id = item.Type + "_" + item.ID;

                        structCash[mcConst.msgTypes.section + "_" + item.ParentID].Item.data.push(item);

                        structCash[item.id] = {
                            Item: item
                        };

                        if (item.Type === mcConst.msgTypes.section) {
                            newIds.push(item.ID);

                            structCash[item.id].Item.data = [];
                            structCash[item.id].Data      = structCash[item.id].Item.data;
                        }

                        data.splice(idx, 1);
                    } else {
                        newData.push(item);

                        idx ++;
                    }
                } else {
                    i ++;
                }

                item = null;
            }

            return data.length > 0 ? serviceList.buildForumStructure(struct, newData, newIds, structCash, exeptTopicks) : struct;
        },

        insertAtCursor: function (textarea, val, isPara) {
            var res = 0;

            if (document.selection) {
                var sel = document.selection.createRange();

                textarea.focus();

                sel.text = val;

                res = sel.text.toString().length - 1;
            } else
            if (textarea.selectionStart || textarea.selectionStart == '0') {
                var startPos = textarea.selectionStart;
                var endPos   = textarea.selectionEnd;

                var prevText = textarea.value.substring(0, startPos);
                var nextText = textarea.value.substring(endPos, textarea.value.length);

                textarea.value =
                    (isPara && prevText.length && prevText[prevText.length - 1] !== "\n" ? prevText + "\n" : prevText) +
                     val +
                    (isPara && nextText.length && nextText[0] !== "\n" ? "\n" + nextText : nextText);

                textarea.selectionStart = startPos + val.length + (isPara ? 1 : 0);
                textarea.selectionEnd   = startPos + val.length + (isPara ? 1 : 0);

                res = textarea.selectionEnd;
            } else {
                textarea.value += (isPara && textarea.value.length && textarea.value[textarea.value.length - 1] !== "\n") ? "\n" + val : val;

                res = textarea.value.length - 1;
            }

            return res;
        },

        parseAttachment: function (text, files, pref, cb, duManager, generateLinks, isComment){
            var link  = "";
            var closeSymbol = "}";
            var isWeb = location.protocol !== 'file:';

            if (text.indexOf(pref) >=0 ){
                text = text.split(pref);

                for (var i = 0; i < text.length; i++){
                    if (!i && text[i].length){
                        if (cb) {
                            text[i] = cb(text[i], files);
                        }

                        continue;
                    }

                    var idx       = text[i].indexOf(closeSymbol);
                    var name      = serviceList.copy(text[i], 0, idx);
                    var endOfLine = serviceList.copy(text[i], idx + 1, text[i].length - 1);
                    var parsedEndLine = "";

                    if (files && files.hasOwnProperty(name)){
                        if (pref === mcConst.prefix.file){
                            link = serviceList.myReplaceFormated(
                                '<a href="#{link}" class="internalLink" title="#{link}" target="_blank">#{name}</a>',
                                {
                                    link: serviceList.getLocalHostPath(isWeb) + "/" + mcConst.pathAliases.AliasFiles + "/kanban/" + files[name].Hash + "/" + name,
                                    name: serviceList.insertIco("fa-file-o", "fs90") + name
                                }
                            );
                        } else

                        if (pref === mcConst.prefix.image){
                            imgIndex++;

                            if (generateLinks){
                                generateLinks(files[name], name);
                            }

                            var resolutionW = files[name].width  ? Math.round(parseInt(files[name].width))  : "auto";
                            var resolutionH = files[name].height ? Math.round(parseInt(files[name].height)) : "auto";

                            if (files[name].width){
                                if (resolutionH === resolutionW && resolutionW <= mcConst.imageSize.big.y){
                                    // nothing 
                                } else
                                if (resolutionH >= resolutionW) {
                                    if (resolutionH > mcConst.imageSize.big.y){
                                        resolutionH = mcConst.imageSize.big.y;
                                    }

                                    resolutionW = Math.round(files[name].width / (files[name].height / mcConst.imageSize.big.y));
                                } else {
                                    if (resolutionW > mcConst.imageSize.big.x){
                                        resolutionW = mcConst.imageSize.big.x;
                                    }

                                    resolutionH = Math.round(files[name].height / (files[name].width / mcConst.imageSize.big.x));
                                }
                            }

                            var now = serviceList.RandomHash(7);

                            link = serviceList.myReplaceFormated(
                                '<div><a href="#{hrefImg}" class="internalLink" target="_blank" style="#{widthPX} #{heightPX};">' + // background-image: url(#{tmp})
                                    '<img #{idClass}="img_temp_#{hash}" src="#{loadImage}" alt="" width="#{width}" height="#{height}" >' +  // style="background-image: url(#{tmp})"
                                '</a></div>',
                                {
                                    idClass  : isComment ? "id" : "class",
                                    tmp      : mcConst.imagesPath.noFile,
                                    width    : resolutionW || "",
                                    height   : resolutionH || "",
                                    widthPX  : "width:"  + (resolutionW && resolutionW !== "auto" ? resolutionW + "px": resolutionW) + ";",
                                    heightPX : "height:" + (resolutionH && resolutionH !== "auto" ? resolutionH + "px": resolutionH) + ";",
                                    hrefImg  : files[name].lnkHref ? files[name].lnkHref : "#",
                                    loadImage: isComment ? "" : (files[name].imgSrc  ? files[name].imgSrc  : ""),
                                    // loadImage: "", //mcConst.imagesPath.noImage,
                                    hash     : files[name].Hash + (isComment ? "_" + now : "")
                                }
                            );

                            duManager.loadThumbs([files[name].Hash, name, null, now]);
                        }

                        if (cb) {
                            parsedEndLine = cb(endOfLine, files);
                        }

                        text[i] = link + (parsedEndLine || endOfLine);
                    } else {
                        if (cb) {
                            parsedEndLine = cb(endOfLine, files);

                            text[i] = link + (parsedEndLine || endOfLine);
                        }
                    }
                }
                
                text = text.join('');
            } else
            if (cb) {
                text = cb(text, files);
            }

            return text;
        },

        checkNumbersCounter: function(code){
            if (this.config.view === "counter" && serviceList.inArray(code, mcConst.keyCodes.Digits.concat(mcConst.keyCodes.EditSymbols)) === -1){
                return false;
            }
        },

        isImage: function (file) {
            return serviceList.inArrayNoStrict(serviceList.extractFileExtension(file).toLowerCase(), ["png", "jpg", "jpeg", "gif", "bmp"]) !== -1;
        },

        getCurrentChantTypeForFiles: function (type) {
            // var currentChat = type;
            var res         = 1;

            switch (type){
                case "UIN"   : res = mcConst.whereFiles.private;break;
                case "UID"   : res = mcConst.whereFiles.conf;   break;
                case "Forum" : res = mcConst.whereFiles.forum;  break;
                case "Kanban": res = mcConst.whereFiles.kanban; break;
                case "BBS"   : res = mcConst.whereFiles.bbs;    break;
            }

            return res;
        },

        copyTextToClipboard: function (text, message) {
            function fallbackCopyTextToClipboard(text) {
                var textArea = document.createElement("textarea");

                textArea.value = text;
                textArea.className = "__terminal-clipcopy";

                document.body.appendChild(textArea);

                textArea.focus();
                textArea.select();

                try {
                    document.execCommand('copy');

                    webix.message('Text copied to clipboard');
                } catch (err) {
                    console.error('Fallback: could not copy text to clipboard: ', err);
                }

                document.body.removeChild(textArea);
            }

            if (!navigator.clipboard) {
                fallbackCopyTextToClipboard(text);

                return;
            }

            navigator.clipboard.writeText(text).then(function() {
                if (message !== false) {
                    webix.message(message || 'Text copied to clipboard');
                }
            }, function(err) {
                console.error('Could not copy text to clipboard: ', err);
            });
        },

        presenterTemp: function (){
            return "<!DOCTYPE html>" +
                "<html lang=\"en\">" +
                "<head>" +
                "    <meta charset=\"UTF-8\">" +
                "    <title>#{title}</title>" +
                "    <link rel=\"stylesheet\" type=\"text/css\" href=\"/mctools/simplemde/simplemde.min.css\">" +
                "    <link rel=\"stylesheet\" type=\"text/css\" href=\"/frameworks/webix/codebase/css/font-awesome.min.css\">" +
                "    <link rel=\"stylesheet\" type=\"text/css\" href=\"/frameworks/webix/codebase/css/github-markdown.css\">" +
                "</head>" +
                "<body class='markdown-body'>" +
                "    #{content}" +
                "    #{mc_sign}" +
                "</body>" +
                "</html>"
            ;
        },

        getSmileNumbByName : function (name) {
            return Object.keys(mcConst.SmileysSet).filter(function (item) {
                if (mcConst.SmileysSet[item].name === name) {
                    return true;
                }
            }).join("") || name;
        },

        getMyStatus: function () {
            switch (mcConst.UserInfo.onlineState) {
                case mcConst.states.online : return mcLang(30);   // "30" :"В сети",
                case mcConst.states.offline: return mcLang(29);   // "29" :"Не в сети",
                case mcConst.states.away   : return mcLang(31);   // "31" :"Нет на месте",
                case mcConst.states.dnd    : return mcLang(32);   // "32" :"Не беспокоить",
                case mcConst.states.webOnline: return mcLang(30); // "30" :"В сети",
            }
        },

        getTwoLettersName : function (name) {
            var rex = null;

            try {
                rex = (new RegExp("((\\p{L}|\\d).+[\\s\\-._()]+(\\p{L}|\\d))|(\\p{L}|\\d)", "iu")).exec(name);
            } catch (e) {
                rex = (new RegExp("((\\S|\\d).+[\\s\\-._()]+(\\S|\\d))|(\\S|\\d)", "i")).exec(name);
            }

            var res = "";

            if (!rex){
                res = (name || "U").toString()[0];
            } else
            if (rex[4]){
                res = rex[4];
            } else {
                res = rex[2] + rex[3];
            }

            return res.toUpperCase();
        },

        Error: function (id, params) {
            return serviceList.myReplace(mcConst.ErrorText[id], params);
        },

        /**
         * @return {boolean}
         */
        CheckLink: function (text) {
            var SCHEME   = "[a-z\\d.-]+://";
            var IPV4     = "(?:(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])\\.){3}(?:[0-9]|[1-9]\\d|1\\d{2}|2[0-4]\\d|25[0-5])";
            var HOSTNAME = "(?:(?:[^\\s!@#$%^&*()_=+[\\]{}\\\\|;:'\",.<>/?]+)\\.)+";
            var TLD      =  "(?:ac|ad|aero|ae|af|ag|ai|al|am|an|ao|aq|arpa|ar|asia|as|at|au|aw|ax|az|ba|bb|bd|be|bf|bg|bh|biz|bi|bj|bm|" +
                    "bn|bo|br|bs|bt|bv|bw|by|bz|cat|ca|cc|cd|cf|cg|ch|ci|ck|cl|cm|cn|coop|com|co|cr|cu|cv|cx|cy|cz|de|dj|dk|dm|" +
                    "do|dz|ec|edu|ee|eg|er|es|et|eu|fi|fj|fk|fm|fo|fr|ga|gb|gd|ge|gf|gg|gh|gi|gl|gm|gn|gov|gp|gq|gr|gs|gt|gu|gw|" +
                    "gy|hk|hm|hn|hr|ht|hu|id|ie|il|im|info|int|in|io|iq|ir|is|it|je|jm|jobs|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|mil|mk|ml|mm|mn|mobi|mo|mp|mq|mr|ms|mt|museum|mu|mv|" +
                    "mw|mx|my|mz|name|na|nc|net|ne|nf|ng|ni|nl|no|np|nr|nu|nz|om|org|pa|pe|pf|pg|ph|pk|pl|pm|pn|pro|pr|ps|pt|pw|" +
                    "py|qa|re|ro|rs|ru|rw|sa|sb|sc|sd|se|sg|sh|si|sj|sk|sl|sm|sn|so|sr|st|su|sv|sy|sz|tc|td|tel|tf|tg|th|tj|tk|" +
                    "tl|tm|tn|to|tp|travel|tr|tt|tv|tw|tz|ua|ug|uk|um|us|uy|uz|va|vc|ve|vg|vi|vn|vu|wf|ws|xn--0zwm56d|xn--11b5bs3a9aj6g|" +
                    "xn--80akhbyknj4f|xn--9t4b11yi5a|xn--deba0ad|xn--g6w251d|xn--hgbk6aj7f53bba|xn--hlcj6aya9esc7a|xn--jxalpdlp|" +
                    "xn--kgbechtv|xn--zckzah|ye|yt|yu|za|zm|zw|dev)";
            var PORT = "(?::\\d{1,5})?";
            var HOST_OR_IP = "(?:" + HOSTNAME + TLD + PORT + "|" + IPV4 + PORT + ")";
            var PATH = "(?:[;/][^#?<>\\s]*)?";
            var QUERY_FRAG = "(?:\\?[^#<>\\s]*)?(?:#[^<>\\s]*)?";
            var URI1 = "\\b" + SCHEME + "[^<>(\\s|\\)|\\(|\\[|\\])]+"; //"[^<>\\s]+";
            var URI2 = "\\b" + HOST_OR_IP + PATH + QUERY_FRAG + "(?!\\w)";

            var MAILTO = "mailto:";
            var EMAIL  = "(?:" + MAILTO + ")?[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@" + HOST_OR_IP + QUERY_FRAG + "(?!\\w)";
            var URI_RE = new RegExp( "^(?:" + URI1 + "|" + URI2 + ")$", "i" ); // "|" + EMAIL +

            return URI_RE.test(text);
        },

        checkNoRestoreTool: function (restoreTool){
            return restoreTool === mcConst.dataModels.Settings         || restoreTool === mcConst.dataModels.UserProfile ||
                   restoreTool === mcConst.dataModels.HistoryBroadcast || restoreTool === mcConst.dataModels.ViewLogs ||
                   restoreTool === mcConst.dataModels.CreateConf;
        },

        tree_icon: function (obj){
            if (obj.$count || obj.needLoad){
                if (obj.open)
                    return "<div class='webix_tree_open'></div>";
                else
                    // return "<div class='_tree_close'></div>";
                    return "<div class='webix_tree_close'></div>";
            } else
                return "<div class='webix_tree_none'></div>";
        },

        IM: function (_uin){
            var uin = parseInt(_uin);
            var me  = parseInt(mcConst.UserInfo.UIN);
            
            return !isNaN(uin) && uin === me;
        },

        checkMediaTypesSupport : function (){
            var types = [
                "audio/webm;codecs=opus",
                "audio/webm",
                
                "video/webm",
                "video/ogg",
                "video/mp4",
                "video/mpeg",

                'video/webm;codecs="vp08.00.41.08,vorbis"',
                'video/webm;codecs="vp09.02.10.10.01.09.16.09.01,opus"',

                "video/webm;codecs=h264",
                "video/webm;codecs=vp8",
                "video/webm;codecs=vp9",
            ];

            for (var i in types) {
                console.log( (MediaRecorder.isTypeSupported(types[i]) ? "Yes" : "No ") + " - " + types[i]);
            }

            console.log("https://developer.mozilla.org/en-US/docs/Web/Media/Formats/codecs_parameter");
        },

        getSearchQuery: function (src, condition, simple) {
            var data = null;

            if (condition === "ref") {
                data = src.split('&ref=')[1];
            } else {
                src = serviceList.copy(src, 1).split('&');

                for (var i = 0; i < src.length; i++){
                    if ((condition && serviceList.isFunction(condition) && condition(src[i])) || !condition || src[i].indexOf(condition + "=") === 0){
                        if (simple) {
                            data = decodeURI(src[i].split('=')[1]);

                            break;
                        } else {
                            if (!data){
                                data = [];
                            }

                            data.push(src[i]);
                        }
                    }
                }
            }

            return data;
        },

        jsonp: function (url, callback, err, term) {
            var callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random());

            window[callbackName] = function(data) {
                delete window[callbackName];

                document.body.removeChild(script);

                callback(data);
            };

            var script  = document.createElement('script');
            script.src  = url +  (term ? term : "&") + 'callback=' + callbackName;
            script.onerror = function (e){
                delete window[callbackName];

                document.body.removeChild(script);

                err && err(e);
            };

            try {
                document.body.appendChild(script);
            } catch (e){}
        },

        getVisualViewPortValues : function (clampOffsets, resizeDimensions) {
            const clamp = (val, min, max) => Math.min(Math.max(val, min), max);
            
            let {
                    width,
                    height,
                    widthOrig = null,
                    heightOrig = null,
                    scale,
                    offsetTop,
                    offsetLeft,
                    offsetTopOrig = null,
                    offsetLeftOrig = null,
                    pageLeft,
                    pageTop,
                    pageLeftOrig = null,
                    pageTopOrig = null,
                } = window.visualViewport;

            // Adjust values by clamping
            if (clampOffsets) {
                // Cache original values
                pageTopOrig = pageTop;
                pageLeftOrig = pageLeft;
                offsetTopOrig = offsetTop;
                offsetLeftOrig = offsetLeft;

                // Offsets measured against the page cannot go negative, nor exceed the max scroll offset
                // @note: Values below only used for absolute positioning
                pageTop = clamp(pageTop, 0, document.body.offsetHeight - height);
                pageLeft = clamp(pageLeft, 0, document.body.offsetWidth - width);

                // Offsets measured against the Layout Viewport cannot go negative, nor exceed the max offset within the Layout Viewport
                // @note: Values below only used for fixed positioning
                const layoutViewportHeight = (window.innerHeight * scale); // @TODO: This only is true when window.innerHeight resizes. Which is not always the case when pinch-zooming (or which happens too late as UA UI shows/hides)
                const layoutViewportWidth = (window.innerWidth * scale); // @TODO: This only is true when window.innerHeight resizes. Which is not always the case when pinch-zooming (or which happens too late as UA UI shows/hides)
                offsetTop = clamp(offsetTop, 0, layoutViewportHeight - height);
                offsetLeft = clamp(offsetLeft, 0, layoutViewportWidth - width);
            }

            // Adjust values by resizing height/width
            // Only needed for browsers that expose overscrolling through scrollX/scrollY
            if (resizeDimensions) {
                widthOrig = width;
                heightOrig = height;
                pageTopOrig = pageTop;
                pageLeftOrig = pageLeft;
                offsetTopOrig = offsetTop;
                offsetLeftOrig = offsetLeft;

                // Mobile Safari: Fixed Viewport does overscroll. Max value can overshoot 0 (inverse of overScrollY) and min value can exceed inverted max scroll distance
                // Desktop Safari: Fixed Viewport does not overscroll. Max value is 0, Min value is inverted max scroll distance.
                // We need a way to distinguish between both, because they need a different fix
                // @TODO: No longer rely on UA sniffing here, and find a way to detect wether the Fixed Viewport can overscroll or not …
                const isMobileSafari = (!!window.navigator.userAgent.match(/iPad/i) || !!window.navigator.userAgent.match(/iPhone/i));
                const isDesktopSafari = !isMobileSafari;

                // Overscrolling at the right edge
                if (width + pageLeft > document.body.offsetWidth) {
                    width = document.body.offsetWidth - pageLeft;

                    // @note: Value below only used for fixed positioning
                    if (isMobileSafari) {
                        if (scale == 1) offsetLeft -= (widthOrig - width);
                        if (offsetLeft < 0) offsetLeft = -offsetLeft;
                    }
                    else {
                        offsetLeft += (widthOrig - width); // Add overscroll value to the offset
                    }
                }

                // Overscrolling at the bottom edge
                if (height + pageTop > document.body.offsetHeight) {
                    height = document.body.offsetHeight - pageTop;

                    // @note: Value below only used for fixed positioning
                    if (isMobileSafari) {
                        if (scale == 1) offsetTop -= (heightOrig - height);
                        if (offsetTop < 0) offsetTop = -offsetTop;
                    }
                    else {
                        offsetTop += (heightOrig - height); // Add overscroll value to the offset
                    }
                }

                // Overscrolling at the left edge
                // @note: Might be tricky to achieve as you might trigger back navigation
                if (pageLeft < 0) {
                    width += pageLeft;

                    // @note: Value below only used for abs position
                    pageLeft = 0;

                    // @note: Value below only used for fixed positioning
                    offsetLeft = Math.abs(offsetLeft);
                }

                // Overscrolling at the top edge
                // @note: Might be tricky to achieve as you might trigger pull-to-refresh
                if (pageTop < 0) {
                    height += pageTop;

                    // @note: Value below only used for abs positioning
                    pageTop = 0;

                    // @note: Value below only used for fixed positioning
                    offsetTop = Math.abs(offsetTop);
                }

            }

            return Object.fromEntries(
                Object.entries({
                    width,
                    widthOrig,
                    height,
                    heightOrig,
                    scale,
                    offsetTop,
                    offsetTopOrig,
                    offsetLeft,
                    offsetLeftOrig,
                    pageLeft,
                    pageLeftOrig,
                    pageTop,
                    pageTopOrig,
                }).filter(([k, v]) => v !== null)
            );
        },

        _min: function (d0, d1, d2, bx, ay) {
            return d0 < d1 || d2 < d1
                ? d0 > d2
                    ? d2 + 1
                    : d0 + 1
                : bx === ay
                    ? d1
                    : d1 + 1;
        },

        levenshteinDistance : function(a, b){
            if (a === b) return 0;

            if (a.length > b.length) {
                let tmp = a;
                a = b;
                b = tmp;
            }

            let la = a.length;
            let lb = b.length;
            let x  = 0;
            let offset = 0;
            let vector = [];
            let y, d0, d1, d2, d3, dd, dy, ay, bx0, bx1, bx2, bx3;

            while (la > 0 && (a.charCodeAt(la - 1) === b.charCodeAt(lb - 1))) {
                la--;
                lb--;
            }

            while (offset < la && (a.charCodeAt(offset) === b.charCodeAt(offset))) {
                offset++;
            }

            la -= offset;
            lb -= offset;

            if (la === 0 || lb < 3) {
                return lb;
            }

            for (y = 0; y < la; y++) {
                vector.push(y + 1);
                vector.push(a.charCodeAt(offset + y));
            }

            let len = vector.length - 1;

            for (; x < lb - 3;) {
                bx0 = b.charCodeAt(offset + (d0 = x));
                bx1 = b.charCodeAt(offset + (d1 = x + 1));
                bx2 = b.charCodeAt(offset + (d2 = x + 2));
                bx3 = b.charCodeAt(offset + (d3 = x + 3));
                dd = (x += 4);
                for (y = 0; y < len; y += 2) {
                    dy = vector[y];
                    ay = vector[y + 1];
                    d0 = _min(dy, d0, d1, bx0, ay);
                    d1 = _min(d0, d1, d2, bx1, ay);
                    d2 = _min(d1, d2, d3, bx2, ay);
                    dd = _min(d2, d3, dd, bx3, ay);
                    vector[y] = dd;
                    d3 = d2;
                    d2 = d1;
                    d1 = d0;
                    d0 = dy;
                }
            }

            for (; x < lb;) {
                bx0 = b.charCodeAt(offset + (d0 = x));
                dd = ++x;
                for (y = 0; y < len; y += 2) {
                    dy = vector[y];
                    vector[y] = dd = _min(dy, d0, dd, bx0, vector[y + 1]);
                    d0 = dy;
                }
            }

            return dd;
        }
    };

    var multiple    = {
        kanban: {
            FilterFiled : function (options){
                function clearFilter(filter, noFocus){
                    if (clearShowed && !options.noCloseBtn) {
                        $$("clearBtn" + rnd).hide();

                        clearShowed = false;
                    }

                    options.thisFilter.setValue('');
                    options.thisFilter.$setValue('');

                    if (!filter) {
                        doFilter(noFocus);
                    } else {
                        options.List.filter("");
                    }

                    if (options.onClear) {
                        options.onClear();
                    }
                }

                function checkParams(text, obj, paramList, strict) {
                    var fp = [];
                    var show = true;

                    for (var i = 0; i < paramList.length; i++){
                        var param = paramList[i];

                        if (serviceList.isArray(obj[param])){
                            fp.push(obj[param].join('|').toLowerCase());
                        } else
                        if (param.indexOf(':') === -1){
                            if (obj[param] !== undefined) fp.push(obj[param].toString().toLowerCase());
                        } else {
                            var _param = param.split(':');

                            fp = fp.concat(obj[_param[0]].toString().toLowerCase().split(_param[1]));
                        }
                    }

                    if (serviceList.isString(text)){
                        if (strict){
                            show = fp.indexOf(text.toString().toLowerCase()) !== -1;
                        } else {
                            show = fp.join('|').indexOf(text) !== -1;
                        }
                    } else

                    if (serviceList.isBoolean(text)){
                        show = fp.indexOf(text.toString().toLowerCase()) !== -1;
                    } else

                    if (serviceList.isArray(text)){
                        for (var i = 0; i < text.length; i ++){
                            if (fp.indexOf(text[i].toString().toLowerCase()) === -1) {
                                show = false;

                                break;
                            }
                        }
                    }

                    return show;
                }

                function doFilter(noFocus) {
                    if (options.List && options.thisFilter) {
                        var text        = serviceList.trim(options.thisFilter.getValue().toString()).toLowerCase();
                        var listFilters = options.List._otherFilterOptions ? Object.keys(options.List._otherFilterOptions) : [];
                        var ofo         = options.List._otherFilterOptions;

                        if (text === "" && !listFilters.length){
                            clearFilter(true);

                            if (!noFocus) {
                                options.thisFilter.focus();
                            }
                        } else {
                            var res = [];
                            var pos = serviceList.getCaretPosition(options.thisFilter.getInputNode());

                            // console.log('=======================================');

                            options.List.filter(function(obj){
                                var filterResult = true;
                                var subFilter    = true;

                                // console.log(obj);
                                // console.log(obj.Tags);

                                if (!options.filterParams || options.filterParams.length === 0){
                                    options.filterParams = ['value'];
                                }

                                if (text !== ""){
                                    filterResult = checkParams(text, obj, options.filterParams);
                                }

                                if (filterResult && listFilters.length){
                                    for (var i = 0; i < listFilters.length; i ++){
                                        var searchText = ofo[listFilters[i]].items;

                                        if (serviceList.isString(searchText)){
                                            subFilter = subFilter && checkParams(searchText, obj, ofo[listFilters[i]].fields);
                                        } else

                                        if (serviceList.isArray(searchText)){
                                            subFilter = ofo[listFilters[i]].union === mcConst.methods.AND;

                                            for (var j = 0; j < searchText.length; j ++){
                                                subFilter = ofo[listFilters[i]].union === mcConst.methods.OR
                                                    ? subFilter || checkParams(searchText[j], obj, ofo[listFilters[i]].fields, true)
                                                    : subFilter && checkParams(searchText[j], obj, ofo[listFilters[i]].fields, true)
                                                ;

                                                if (ofo[listFilters[i]].union === mcConst.methods.AND){
                                                    break;
                                                }
                                            }
                                        }

                                        if (!subFilter) {
                                            break;
                                        }
                                    }
                                }

                                filterResult = filterResult && subFilter;

                                return filterResult ? !!res.push(obj) : false;
                            });

                            if (options.onAfterFilter){
                                options.onAfterFilter(res);

                                res = null;
                            }
                        }

                        if (!clearShowed && text !== "" && !options.noCloseBtn) {
                            clearShowed = true;

                            $$("clearBtn" + rnd).show();

                            if (!noFocus) setTimeout(function () {
                                options.thisFilter.focus();

                                serviceList.setCaretPosition(options.thisFilter.getInputNode(), pos);
                            }, 5);
                        }
                    }
                }

                var clearShowed = false;
                var rnd         = Math.random();
                var _filter     = { rows: [
                    { height: (options.topSpacer === undefined)? 0 : options.topSpacer },

                    { cols: [
                        { view: "search", padding: 0, css: 'noPaddingChild', placeholder: options.searchText || serviceList.Lang(10), keyPressTimeout: 400, on: { // "10" :"Введите текст для поиска",
                            onKeyPress: function (code, e) {
                                if (code === 27 && !e.ctrlKey && !e.shiftKey && !e.altKey) {
                                    var needFocus = this.getValue().length;

                                    clearFilter();

                                    if (needFocus) {
                                        this.focus();
                                    }
                                }
                            },
                            onTimedKeypress : function () {
                                doFilter();
                            },
                            onAfterRender : function closeFilter(){
                                options.thisFilter = this;
                                _filter.thisFilter = this;
                                
                                options.List = $$(options.listName);

                                setTimeout(function () {
                                    _filter.thisFilter.getInputNode().setAttribute("autocomplete", "off");
                                }, 100);
                            }
                        }},

                        { id: "clearBtn" + rnd, view: "button", type: "iconButton", icon: "times", width: 27, hidden: true, click: function () {
                            setTimeout(function () {
                                clearFilter();
                            }, 50);
                        }}
                    ]},

                    { height: options.bottomSpacer === undefined ? 4 : options.bottomSpacer }
                ]};

                if (options.id){
                    _filter.id = options.id;
                }

                if (options.rows){
                    _filter = serviceList.Marge(_filter, options.rows);
                }

                _filter.relocateList = function (newList, noFocus) {
                    var needClear = !!options.List;

                    options.listName = newList;
                    options.List = $$(newList);

                    if (options.List){
                        if (needClear) {
                            clearFilter(null, noFocus);
                        }
                    } else {
                        options.listName = "";
                        options.List = null;
                    }
                };

                _filter.doFilter = doFilter;
                _filter.clearFilter = clearFilter;

                return _filter;
            },
            createLink  : function (projectID, taskID, commentID) {
                return serviceList.isIE()
                    ? serviceList.myReplaceFormated("#{protocol}//#{host}#{path}#project=#{project}#{task}",
                        {
                            protocol: window.location.protocol,
                            host    : window.location.host,
                            path    : window.location.pathname,
                            project : projectID,
                            task    : taskID ? "&task=" + taskID + (commentID ? "&comment=" + commentID : "") : ""
                        })
                    : serviceList.myReplaceFormated("#{origin}#{path}#project=#{project}#{task}",
                        {
                            origin  : window.location.origin,
                            path    : window.location.pathname,
                            project : projectID,
                            task    : taskID ? "&task=" + taskID + (commentID ? "&comment=" + commentID : "") : ""
                        });
            },
            insertIco   : function (ico, size, attr){
                return "<span class='webix_icon byCenter " + ico + " " + ((size) ? size : "fa-lg") + "' " + (attr ? (serviceList.isString(attr) ? attr : attr.join(' ')) : "") + "></span>"
            }
        },

        admin: {
            FilterFiled : function (options){
                function clearFilter(){
                    if (clearShowed) {
                        $$("clearBtn" + rnd).hide();

                        clearShowed = false;
                    }

                    options.prevText = "";

                    options.thisFilter.$setValue('');
                    options.List.filter("");

                    if (options.onClear) {
                        options.onClear();
                    }
                }

                function doFilter(reFilter, noFocus) {
                    if (options.thisFilter){
                        var saveText = options.thisFilter.getValue().toString();
                        var text = serviceList.trim(saveText).toLowerCase();

                        if (text == ""){
                            clearFilter();

                            if (!options.escPressed && !noFocus) {
                                options.thisFilter.focus();
                            }
                        } else {
                            var res = [];

                            if (!options.prevText || options.prevText != text || reFilter){
                                options.prevText = text;

                                if (options.onBeforeFilter){
                                    options.onBeforeFilter(text);
                                }

                                options.List.filter(function(obj){
                                    var fp = [];

                                    if (!options.filterParams || options.filterParams.length == 0){
                                        options.filterParams = ['value'];
                                    }

                                    for (var i = 0; i < options.filterParams.length; i++){
                                        if (serviceList.isArray(obj[options.filterParams[i]])){
                                            fp.push(obj[options.filterParams[i]].join('|'));
                                        } else {
                                            fp.push(obj[options.filterParams[i]]);
                                        }
                                    }

                                    var filter = fp.join('|');

                                    filter = filter.toString().toLowerCase();

                                    if (filter.indexOf(text) != -1) {
                                        res.push(obj);
                                    }

                                    return (filter.indexOf(text) != -1);
                                });

                                if (options.onAfterFilter){
                                    options.onAfterFilter(res);

                                    res = null;
                                }

                                if (!clearShowed) {
                                    clearShowed = true;

                                    var pos = serviceList.getCaretPosition(options.thisFilter.getInputNode());

                                    $$("clearBtn" + rnd).show();

                                    options.thisFilter.$setValue(saveText);

                                    if (!noFocus) {
                                        options.thisFilter.focus();

                                        serviceList.setCaretPosition(options.thisFilter.getInputNode(), pos);
                                    }
                                }
                            }
                        }
                    }
                }

                var rnd = Math.random();
                var clearShowed = false;
                var _filter = { rows: [
                    { cols: [
                        { view: "text", padding: 0, css: 'noPaddingChild', tooltip: options.tooltip || "", placeholder: options.searchText || serviceList.Lang(71), keyPressTimeout: 300, on: { // "71" : "Введите текст для поиска"
                            onKeyPress: function (code, e) {
                                options.escPressed = false;

                                if (code === 27 && !e.ctrlKey && !e.shiftKey && !e.altKey) {
                                    options.escPressed = true;

                                    options.nowCleared = options.thisFilter.getValue().toString() != "";

                                    clearFilter();

                                    if (options.nowCleared) {
                                        this.focus();
                                    } else {
                                        this.blur();

                                        webix.UIManager.setFocus(document);
                                    }
                                }
                            },
                            onTimedKeypress : function () {
                                if (webix.UIManager.hasFocus(this)) {
                                    doFilter();
                                }
                            },
                            onAfterRender : function closeFilter(){
                                options.thisFilter = this;
                                _filter.thisFilter = this;
                                options.List       = $$(options.listName);

                                this.getInputNode().setAttribute("autocomplete", "off");
                            }
                        }},

                        { id: "clearBtn" + rnd, view: "button", type: "iconButton", icon: "times", height: 28, width: 27, hidden: true, click: clearFilter } // , css: "noBorder webixBlue"
                    ]},

                    { height: (options.bottomSpacer == undefined)? 4 : options.bottomSpacer }
                ]};

                if (options.id){
                    _filter.id = options.id;
                }

                _filter.doFilter = doFilter;
                _filter.clearFilter = clearFilter;

                return _filter;
            },
            checkRights : function (_id){
                var id = parseInt(_id) - 1;

                return mcConst.MyRightsSet.RightsSet[id] === '1' && (!_gLock.navAccess || (_gLock.navAccess && !_gLock.navAccess[_id]));
            }
        },

        chat: {
            FilterFiled : function (options){
                function clearFilter(){
                    if (clearShowed) {
                        $$(CLEAR).hide();

                        clearShowed = false;
                    }

                    options.thisFilter.$setValue('');
                    options.List.filter("");

                    if (options.onClear) {
                        options.onClear();
                    }
                }

                var rnd         = serviceList.RandomHash(6);
                var EDITOR      = (options.id || rnd) + "Editor";
                var CLEAR       = "clearBtn" + (options.clearID || rnd);
                var clearShowed = false;

                var _filter     = { id: options.id || (rnd + 'FilterBlock'), rows: [
                    { cols: [
                        { id: EDITOR, view: "text", padding: 0, css: 'noPaddingChild', placeholder: options.placeholder || "Search", keyPressTimeout: 500, on: { // "71" : "Введите текст для поиска"
                            onKeyPress: function(code, e){
                                if (options.keyEvents && options.keyEvents.hasOwnProperty(code)){
                                    if (options.keyEvents[code](options.List.getSelectedItem())) {
                                        e.preventDefault();
                                    }
                                }
                            },

                            onTimedKeypress : function(){
                                var origin = serviceList.trim(this.getValue().toString());
                                var text   = origin.toLowerCase();

                                if (text === ""){
                                    clearFilter();

                                    this.focus();
                                } else {
                                    var pos = serviceList.getCaretPosition(this.getInputNode());
                                    
                                    options.List.filter(function(obj){
                                        var fp = [];

                                        if (!options.filterParams || options.filterParams.length === 0){
                                            options.filterParams = ['value'];
                                        }

                                        for (var i = 0; i < options.filterParams.length; i++){
                                            if (serviceList.isArray(obj[options.filterParams[i]])){
                                                fp.push(obj[options.filterParams[i]].join('|'));
                                            } else {
                                                fp.push(obj[options.filterParams[i]]);
                                            }
                                        }

                                        var filter = fp.join('|');

                                        filter = filter.toString().toLowerCase();

                                        return (filter.indexOf(text) !== -1);
                                    });

                                    if (!clearShowed) {
                                        clearShowed = true;

                                        if (!options.noClearBtn) {
                                            $$(CLEAR).show();
                                        }

                                        // var pos = serviceList.getCaretPosition(this.getInputNode());

                                        this.$setValue(origin);
                                        this.focus();

                                        serviceList.setCaretPosition(options.thisFilter.getInputNode(), pos);
                                    }
                                }

                                if (options.OnEnterText){
                                    options.OnEnterText(origin);
                                }
                            },
                            onAfterRender : function(){
                                options.thisFilter = this;
                                options.List       = $$(options.listName);
                                options.hotkeyID   = serviceList.RandomHash(10);

                                $$(options.id || (rnd + 'FilterBlock')).onClear = function (cb) {
                                    options.onClear = cb;
                                };

                                if (serviceList._hotkeys){
                                    serviceList._hotkeys.addPreset(options.hotkeyID, [{
                                        key : mcConst.keyCodes.esc,
                                        // lockPrev: true,
                                        func: function closeFilter() {
                                            if (options.thisFilter.getValue().toString() !== "") {
                                                options.nowCleared = true;
                                            }

                                            clearFilter();

                                            options.thisFilter.focus();
                                        }
                                    }], options.thisFilter.getNode());
                                }
                            },
                            onDestruct: function () {
                                serviceList._hotkeys.removePreset(options.hotkeyID);
                            }
                        }},

                        { id: CLEAR, view: "button", type: "iconButton", icon: "times", width: 27, hidden: true, on: {onItemClick: clearFilter}, css: "icoButton button_primary byCenter iconNoPadding" } //
                    ]},

                    { height: (options.bottomSpacer == undefined)? 4 : options.bottomSpacer }
                ]};

                _filter.EDITOR = EDITOR;
                _filter.CLEAR  = CLEAR ;
                _filter.clearFilter = clearFilter;

                return _filter;
            }
        },

        forum: {
            FilterFiled : function (options){
                function clearFilter(){
                    if (clearShowed) {
                        $$("clearBtn" + rnd).hide();

                        clearShowed = false;
                    }

                    options.thisFilter.$setValue('');
                    options.List.filter("");

                    if (options.onClear) {
                        options.onClear();
                    }
                }

                var rnd = Math.random();
                var clearShowed = false;
                var res = { rows: [
                    { height: (options.topSpacer === undefined)? 0 : options.topSpacer },

                    { cols: [
                        { view: "search", padding: 0, css: 'noPaddingChild', placeholder: serviceList.Lang(options.placeHolder || 10), keyPressTimeout: 600, on: { // "10" :"Введите текст для поиска",
                            onKeyPress: function (code, e) {
                                if (code === 27 && !e.ctrlKey && !e.shiftKey && !e.altKey) {
                                    clearFilter();

                                    this.focus();
                                }
                            },
                            onTimedKeypress : function(){
                                var text = serviceList.trim(this.getValue().toString()).toLowerCase();

                                if (!options.List) {
                                    options.List = $$(options.listName);
                                }

                                if (text == ""){
                                    clearFilter();

                                    this.focus();
                                } else {
                                    options.List.filter(function(obj){
                                        var fp = [];

                                        if (!options.filterParams || options.filterParams.length == 0){
                                            options.filterParams = ['value'];
                                        }

                                        for (var i = 0; i < options.filterParams.length; i++){
                                            fp.push(obj[options.filterParams[i]]);
                                        }

                                        var filter = fp.join('|');

                                        filter = filter.toString().toLowerCase();

                                        return (filter.indexOf(text) != -1);
                                    });

                                    if (!clearShowed) {
                                        clearShowed = true;

                                        var pos = serviceList.getCaretPosition(this.getInputNode());

                                        $$("clearBtn" + rnd).show();

                                        this.$setValue(text);
                                        this.focus();

                                        serviceList.setCaretPosition(this.getInputNode(), pos);
                                    }
                                }
                            },
                            onAfterRender : function(){
                                options.thisFilter = this;
                                options.List = $$(options.listName);
                            }
                        }},

                        { id: "clearBtn" + rnd, view: "button", type: "iconButton", icon: "times", width: 27, hidden: true, click: clearFilter } // , css: "noBorder webixBlue"
                    ]},

                    { height: (options.bottomSpacer == undefined)? 4 : options.bottomSpacer }
                ]};

                if (options.id){
                    res.id = options.id;
                }

                if (options.rows){
                    res = serviceList.Marge(res, options.rows);
                }

                res.relocateList = function (newList) {
                    options.List = $$(newList);

                    clearFilter();
                };

                return res;
            },
            createLink  : function (path) {
                return serviceList.isIE()
                    ? window.location.protocol + "//" + window.location.host + window.location.pathname + "#" + path
                    : window.location.origin + window.location.pathname + "#" + path;
            },
            disableContextMenu : function (_view, exception){
                webix.event(_view.$view, "contextmenu", function(e){
                    function emersion(target, count) {
                        var res = [];
                        var parent = target.parentElement;

                        if (parent){
                            res = res.concat(parent.className.split(" "));

                            if (count){
                                count --;
                            }

                            if (parent.nodeName !== "#document" && count > 0){
                                res = res.concat(emersion(parent, count));
                            }
                        }

                        return res;
                    }

                    function skipItems(items, where, target) {
                        var flag = true;

                        if (items && items.length){
                            items.forEach(function (item) {
                                if ((where.length && where.indexOf(item) !== -1) || emersion(target, 4).indexOf(item) !== -1){
                                    flag = false;
                                }
                            });
                        }

                        return flag;
                    }

                    if (((e.target.nodeName !== "INPUT") && (e.target.nodeName !== "TEXTAREA") && skipItems(exception, e.target.className.split(" "), e.target))
                        || (e.target.type === "button")) {
                        e.preventDefault();
                    }
                });
            }
        }
    };

    if (_api && multiple[_api]){
        Object.keys(multiple[_api]).forEach(function (keyName) {
            serviceList[keyName] = multiple[_api][keyName];
        });
    }

    return serviceList;
}


if (typeof exports === 'object') {
    exports.srv = servicesController();
}
/**
 * Created by Gifer on 21.02.2017.
 */

function mcStorage($rootScope) {
    "use strict";

    var callBackFunctions = {};

    var globalOptions = {};

    function sendData(data) {
        $rootScope.$broadcast('sendCMDToElectron', [data.cmd].concat(data.values).concat([
            function (res) {
                if (res && callBackFunctions[res.name] && callBackFunctions[res.name].length && res.name !== "null") {
                    callBackFunctions[res.name].shift()(res.data);
                } else if (data && data.cb) {
                    data.cb(res);
                }
            }
        ]));
    }

    // function getBrowserWindowFromWebContents(remote, contents) {
    //     var topContents = contents;
    //
    //     while (topContents.hostWebContents) {
    //         topContents = topContents.hostWebContents;
    //     }
    //
    //     return remote.BrowserWindow.fromWebContents(topContents);
    // }

    function universalStorage() {
        return {
            load: function (serverID, uin, name, cb) {
                if ($rootScope.isWebClient) {
                    try{
                        if (cb) {
                            cb(webix.storage.local.get(name + uin));
                        } else {
                            return webix.storage.local.get(name + uin);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    if (!callBackFunctions[name]) {
                        callBackFunctions[name] = [];
                    }

                    callBackFunctions[name].push(cb);

                    sendData({
                        cmd: mcConst._CMD_.ce_storage_get,
                        values: [
                            serverID,
                            uin,
                            name
                        ],
                        cb: cb
                    });
                }
            },

            remove: function (serverID, uin, name, cb) {
                if ($rootScope.isWebClient) {
                    try {
                        if (cb) {
                            cb(webix.storage.local.remove(name + uin));
                        } else {
                            return webix.storage.local.remove(name + uin);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    sendData({
                        cmd: mcConst._CMD_.ce_storage_remove,
                        values: [
                            serverID,
                            uin,
                            name
                        ],
                        cb: cb
                    });
                }
            },

            save: function (serverID, uin, name, data, cb) {
                if (!data) {
                    this.remove(serverID, uin, name, cb);
                } else
                if ($rootScope.isWebClient) {
                    try {
                        if (cb) {
                            cb(webix.storage.local.put(name + uin, data));
                        } else {
                            return webix.storage.local.put(name + uin, data);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    sendData({
                        cmd: mcConst._CMD_.ce_storage_save,
                        values: [
                            serverID,
                            uin,
                            name,
                            data
                        ],
                        cb: cb
                    });
                }
            }
        };
    }

    function saveOptions(data, cb) {
        globalOptions = mcService.Marge(globalOptions, data || {});

        if (!mcConst.ServerInfo.ID && !$rootScope.isWebClient){
            console.log("mcConst.ServerInfo.ID : " + mcConst.ServerInfo.ID);
        }

        if ($rootScope.isWebClient) {
            var ID = mcConst.ServerInfo.ID + mcConst.UserInfo.UIN + mcConst.storageOpts.MAIN_OPTIONS;

            try{
                if (cb) {
                    cb(webix.storage.local.put(ID, JSON.stringify(globalOptions)));
                } else {
                    return webix.storage.local.put(ID, JSON.stringify(globalOptions));
                }
            } catch (e){
                console.error(e.message);
            }
        } else {
            sendData({
                cmd: mcConst._CMD_.ce_storage_save,
                values: [
                    mcConst.ServerInfo.ID,
                    mcConst.UserInfo.UIN,
                    mcConst.storageOpts.MAIN_OPTIONS,

                    JSON.stringify(globalOptions)
                ],
                cb: cb
            });
        }
    }

    function loadOptions(cb) {
        if (globalOptions && mcService.isObject(globalOptions) && !mcService.isObjectEmpty(globalOptions)) {
            cb && cb(mcService.Marge({}, globalOptions));
        } else

        if ($rootScope.isWebClient) {
            try {
                globalOptions = webix.storage.local.get(mcConst.ServerInfo.ID + mcConst.UserInfo.UIN + mcConst.storageOpts.MAIN_OPTIONS) || {};
            } catch (e){
                console.error(e.message);
            }

            if (!mcService.isObject(globalOptions) && globalOptions !== "") {
                globalOptions = {};
            }

            if (cb) {
                cb(mcService.Marge({}, globalOptions));
            } else {
                return mcService.Marge({}, globalOptions);
            }
        } else {
            sendData({
                cmd: mcConst._CMD_.ce_storage_get,
                values: [
                    mcConst.ServerInfo.ID,
                    mcConst.UserInfo.UIN,
                    mcConst.storageOpts.MAIN_OPTIONS,
                ],
                cb: function (data) {
                    globalOptions = mcService.StringToObj(data.data) || {};

                    cb && cb(mcService.Marge({}, globalOptions));
                }
            });
        }
    }

    $rootScope.Storage = {
        msgTemplate : universalStorage(),
        saveWndSize : function (cb) {
            if (!$rootScope.isWebClient) {
                require("electron").ipcRenderer.invoke('getWindowWH').then(function (resWH){
                    // console.log("setSizes= " + JSON.stringify(resWH));

                    sendData({
                        cmd: mcConst._CMD_.ce_storage_save,
                        values: [
                            null,
                            null,
                            "WindowSizes",
                            resWH, //[w, h]
                        ],
                        cb: cb
                    });
                });
            }
        },
        historyDialogs: {
            load: function (uin, name, cb) {
                if ($rootScope.isWebClient) {
                    try {
                        if (cb) {
                            cb(webix.storage.local.get(name + uin));
                        } else {
                            return webix.storage.local.get(name + uin);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    if (!callBackFunctions[name]) {
                        callBackFunctions[name] = [];
                    }

                    callBackFunctions[name].push(cb);

                    sendData({
                        cmd: mcConst._CMD_.ce_history_get_dialogs,
                        values: [],
                        cb: cb
                    });
                }
            },

            remove: function (uin, name, cb) {
                if ($rootScope.isWebClient) {
                    try {
                        if (cb) {
                            cb(webix.storage.local.remove(name + uin));
                        } else {
                            return webix.storage.local.remove(name + uin);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    sendData({
                        cmd: mcConst._CMD_.ce_history_remove_dialogs,
                        values: [],
                        cb: cb
                    });
                }
            },

            save: function (uin, name, data, cb) {
                if ($rootScope.isWebClient) {
                    try {
                        if (cb) {
                            cb(webix.storage.local.put(name + uin, data));
                        } else {
                            return webix.storage.local.put(name + uin, data);
                        }
                    } catch (e){
                        console.error(e.message);
                    }
                } else {
                    sendData({
                        cmd: mcConst._CMD_.ce_history_set_dialogs,
                        values: [
                            data
                        ],
                        cb: cb
                    });
                }
            }
        },
        dialogs     : universalStorage(),
        contactsTree: universalStorage(),
        statistics  : {
            load: function (serverID, uin, cb) {
                if (!$rootScope.isWebClient) {
                    if (!callBackFunctions[name]) {
                        callBackFunctions[name] = [];
                    }

                    callBackFunctions[name].push(cb);

                    sendData({
                        cmd: mcConst._CMD_.ce_statistics_get,
                        values: [
                            serverID,
                            uin,
                        ],
                        cb: cb
                    });
                }
            },

            save: function (serverID, uin, stat, cb) { // mcConst.ServerInfo.ID
                if (!$rootScope.isWebClient) {
                    sendData({
                        cmd: mcConst._CMD_.ce_statistics_save,
                        values: [
                            serverID,
                            uin,
                            stat
                        ],
                        cb: cb
                    });
                }
            }
        },
        options     : {
            load: loadOptions,
            save: saveOptions
        },
        popularSmiles: universalStorage()
    };

    window.mcStorage = $rootScope.Storage;
}

if (!isIE()) (function (exports) {
    'use strict';

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
    /* AES counter-mode (CTR) implementation in JavaScript                (c) Chris Veness 2005-2019  */
    /*                                                                                   MIT Licence  */
    /* www.movable-type.co.uk/scripts/aes.html                                                        */
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

    /* global WorkerGlobalScope */

    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */
    /* AES implementation in JavaScript                                   (c) Chris Veness 2005-2019  */
    /*                                                                                   MIT Licence  */
    /* www.movable-type.co.uk/scripts/aes.html                                                        */
    /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -  */

    /**
     * AES (Rijndael cipher) encryption routines reference implementation,
     *
     * This is an annotated direct implementation of FIPS 197, without any optimisations. It is
     * intended to aid understanding of the algorithm rather than for production use.
     *
     * While it could be used where performance is not critical, I would recommend using the ‘Web
     * Cryptography API’ (developer.mozilla.org/en-US/docs/Web/API/SubtleCrypto/encrypt) for the browser,
     * or the ‘crypto’ library (nodejs.org/api/crypto.html#crypto_class_cipher) in Node.js.
     *
     * See csrc.nist.gov/publications/fips/fips197/fips-197.pdf
     */

    var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();

    function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }

    function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }

    function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }

    var Aes = function () {
        function Aes() {
            _classCallCheck(this, Aes);
        }

        _createClass(Aes, null, [{
            key: 'cipher',


            /**
             * AES Cipher function: encrypt 'input' state with Rijndael algorithm [§5.1];
             *   applies Nr rounds (10/12/14) using key schedule w for 'add round key' stage.
             *
             * @param   {number[]}   input - 16-byte (128-bit) input state array.
             * @param   {number[][]} w - Key schedule as 2D byte-array (Nr+1 × Nb bytes).
             * @returns {number[]}   Encrypted output state array.
             */
            value: function cipher(input, w) {
                var Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES)
                var Nr = w.length / Nb - 1; // no of rounds: 10/12/14 for 128/192/256-bit keys

                var state = [[], [], [], []]; // initialise 4×Nb byte-array 'state' with input [§3.4]
                for (var i = 0; i < 4 * Nb; i++) {
                    state[i % 4][Math.floor(i / 4)] = input[i];
                }state = Aes.addRoundKey(state, w, 0, Nb);

                for (var round = 1; round < Nr; round++) {
                    state = Aes.subBytes(state, Nb);
                    state = Aes.shiftRows(state, Nb);
                    state = Aes.mixColumns(state, Nb);
                    state = Aes.addRoundKey(state, w, round, Nb);
                }

                state = Aes.subBytes(state, Nb);
                state = Aes.shiftRows(state, Nb);
                state = Aes.addRoundKey(state, w, Nr, Nb);

                var output = new Array(4 * Nb); // convert state to 1-d array before returning [§3.4]
                for (var _i = 0; _i < 4 * Nb; _i++) {
                    output[_i] = state[_i % 4][Math.floor(_i / 4)];
                }return output;
            }

            /**
             * Perform key expansion to generate a key schedule from a cipher key [§5.2].
             *
             * @param   {number[]}   key - Cipher key as 16/24/32-byte array.
             * @returns {number[][]} Expanded key schedule as 2D byte-array (Nr+1 × Nb bytes).
             */

        }, {
            key: 'keyExpansion',
            value: function keyExpansion(key) {
                var Nb = 4; // block size (in words): no of columns in state (fixed at 4 for AES)
                var Nk = key.length / 4; // key length (in words): 4/6/8 for 128/192/256-bit keys
                var Nr = Nk + 6; // no of rounds: 10/12/14 for 128/192/256-bit keys

                var w = new Array(Nb * (Nr + 1));
                var temp = new Array(4);

                // initialise first Nk words of expanded key with cipher key
                for (var i = 0; i < Nk; i++) {
                    var r = [key[4 * i], key[4 * i + 1], key[4 * i + 2], key[4 * i + 3]];
                    w[i] = r;
                }

                // expand the key into the remainder of the schedule
                for (var _i2 = Nk; _i2 < Nb * (Nr + 1); _i2++) {
                    w[_i2] = new Array(4);
                    for (var t = 0; t < 4; t++) {
                        temp[t] = w[_i2 - 1][t];
                    } // each Nk'th word has extra transformation
                    if (_i2 % Nk == 0) {
                        temp = Aes.subWord(Aes.rotWord(temp));
                        for (var _t = 0; _t < 4; _t++) {
                            temp[_t] ^= Aes.rCon[_i2 / Nk][_t];
                        }
                    }
                    // 256-bit key has subWord applied every 4th word
                    else if (Nk > 6 && _i2 % Nk == 4) {
                        temp = Aes.subWord(temp);
                    }
                    // xor w[i] with w[i-1] and w[i-Nk]
                    for (var _t2 = 0; _t2 < 4; _t2++) {
                        w[_i2][_t2] = w[_i2 - Nk][_t2] ^ temp[_t2];
                    }
                }

                return w;
            }

            /**
             * Apply SBox to state S [§5.1.1].
             *
             * @private
             */

        }, {
            key: 'subBytes',
            value: function subBytes(s, Nb) {
                for (var r = 0; r < 4; r++) {
                    for (var c = 0; c < Nb; c++) {
                        s[r][c] = Aes.sBox[s[r][c]];
                    }
                }
                return s;
            }

            /**
             * Shift row r of state S left by r bytes [§5.1.2].
             *
             * @private
             */

        }, {
            key: 'shiftRows',
            value: function shiftRows(s, Nb) {
                var t = new Array(4);
                for (var r = 1; r < 4; r++) {
                    for (var c = 0; c < 4; c++) {
                        t[c] = s[r][(c + r) % Nb];
                    } // shift into temp copy
                    for (var _c = 0; _c < 4; _c++) {
                        s[r][_c] = t[_c];
                    } // and copy back
                } // note that this will work for Nb=4,5,6, but not 7,8 (always 4 for AES):
                return s; // see asmaes.sourceforge.net/rijndael/rijndaelImplementation.pdf
            }

            /**
             * Combine bytes of each col of state S [§5.1.3].
             *
             * @private
             */

        }, {
            key: 'mixColumns',
            value: function mixColumns(s, Nb) {
                for (var c = 0; c < Nb; c++) {
                    var a = new Array(Nb); // 'a' is a copy of the current column from 's'
                    var b = new Array(Nb); // 'b' is a•{02} in GF(2^8)
                    for (var r = 0; r < 4; r++) {
                        a[r] = s[r][c];
                        b[r] = s[r][c] & 0x80 ? s[r][c] << 1 ^ 0x011b : s[r][c] << 1;
                    }
                    // a[n] ^ b[n] is a•{03} in GF(2^8)
                    s[0][c] = b[0] ^ a[1] ^ b[1] ^ a[2] ^ a[3]; // {02}•a0 + {03}•a1 + a2 + a3
                    s[1][c] = a[0] ^ b[1] ^ a[2] ^ b[2] ^ a[3]; // a0 • {02}•a1 + {03}•a2 + a3
                    s[2][c] = a[0] ^ a[1] ^ b[2] ^ a[3] ^ b[3]; // a0 + a1 + {02}•a2 + {03}•a3
                    s[3][c] = a[0] ^ b[0] ^ a[1] ^ a[2] ^ b[3]; // {03}•a0 + a1 + a2 + {02}•a3
                }
                return s;
            }

            /**
             * Xor Round Key into state S [§5.1.4].
             *
             * @private
             */

        }, {
            key: 'addRoundKey',
            value: function addRoundKey(state, w, rnd, Nb) {
                for (var r = 0; r < 4; r++) {
                    for (var c = 0; c < Nb; c++) {
                        state[r][c] ^= w[rnd * 4 + c][r];
                    }
                }
                return state;
            }

            /**
             * Apply SBox to 4-byte word w.
             *
             * @private
             */

        }, {
            key: 'subWord',
            value: function subWord(w) {
                for (var i = 0; i < 4; i++) {
                    w[i] = Aes.sBox[w[i]];
                }return w;
            }

            /**
             * Rotate 4-byte word w left by one byte.
             *
             * @private
             */

        }, {
            key: 'rotWord',
            value: function rotWord(w) {
                var tmp = w[0];
                for (var i = 0; i < 3; i++) {
                    w[i] = w[i + 1];
                }w[3] = tmp;
                return w;
            }
        }]);

        return Aes;
    }();

    // sBox is pre-computed multiplicative inverse in GF(2^8) used in subBytes and keyExpansion [§5.1.1]


    Aes.sBox = [0x63, 0x7c, 0x77, 0x7b, 0xf2, 0x6b, 0x6f, 0xc5, 0x30, 0x01, 0x67, 0x2b, 0xfe, 0xd7, 0xab, 0x76, 0xca, 0x82, 0xc9, 0x7d, 0xfa, 0x59, 0x47, 0xf0, 0xad, 0xd4, 0xa2, 0xaf, 0x9c, 0xa4, 0x72, 0xc0, 0xb7, 0xfd, 0x93, 0x26, 0x36, 0x3f, 0xf7, 0xcc, 0x34, 0xa5, 0xe5, 0xf1, 0x71, 0xd8, 0x31, 0x15, 0x04, 0xc7, 0x23, 0xc3, 0x18, 0x96, 0x05, 0x9a, 0x07, 0x12, 0x80, 0xe2, 0xeb, 0x27, 0xb2, 0x75, 0x09, 0x83, 0x2c, 0x1a, 0x1b, 0x6e, 0x5a, 0xa0, 0x52, 0x3b, 0xd6, 0xb3, 0x29, 0xe3, 0x2f, 0x84, 0x53, 0xd1, 0x00, 0xed, 0x20, 0xfc, 0xb1, 0x5b, 0x6a, 0xcb, 0xbe, 0x39, 0x4a, 0x4c, 0x58, 0xcf, 0xd0, 0xef, 0xaa, 0xfb, 0x43, 0x4d, 0x33, 0x85, 0x45, 0xf9, 0x02, 0x7f, 0x50, 0x3c, 0x9f, 0xa8, 0x51, 0xa3, 0x40, 0x8f, 0x92, 0x9d, 0x38, 0xf5, 0xbc, 0xb6, 0xda, 0x21, 0x10, 0xff, 0xf3, 0xd2, 0xcd, 0x0c, 0x13, 0xec, 0x5f, 0x97, 0x44, 0x17, 0xc4, 0xa7, 0x7e, 0x3d, 0x64, 0x5d, 0x19, 0x73, 0x60, 0x81, 0x4f, 0xdc, 0x22, 0x2a, 0x90, 0x88, 0x46, 0xee, 0xb8, 0x14, 0xde, 0x5e, 0x0b, 0xdb, 0xe0, 0x32, 0x3a, 0x0a, 0x49, 0x06, 0x24, 0x5c, 0xc2, 0xd3, 0xac, 0x62, 0x91, 0x95, 0xe4, 0x79, 0xe7, 0xc8, 0x37, 0x6d, 0x8d, 0xd5, 0x4e, 0xa9, 0x6c, 0x56, 0xf4, 0xea, 0x65, 0x7a, 0xae, 0x08, 0xba, 0x78, 0x25, 0x2e, 0x1c, 0xa6, 0xb4, 0xc6, 0xe8, 0xdd, 0x74, 0x1f, 0x4b, 0xbd, 0x8b, 0x8a, 0x70, 0x3e, 0xb5, 0x66, 0x48, 0x03, 0xf6, 0x0e, 0x61, 0x35, 0x57, 0xb9, 0x86, 0xc1, 0x1d, 0x9e, 0xe1, 0xf8, 0x98, 0x11, 0x69, 0xd9, 0x8e, 0x94, 0x9b, 0x1e, 0x87, 0xe9, 0xce, 0x55, 0x28, 0xdf, 0x8c, 0xa1, 0x89, 0x0d, 0xbf, 0xe6, 0x42, 0x68, 0x41, 0x99, 0x2d, 0x0f, 0xb0, 0x54, 0xbb, 0x16];

    // rCon is Round Constant used for the Key Expansion [1st col is 2^(r-1) in GF(2^8)] [§5.2]
    Aes.rCon = [[0x00, 0x00, 0x00, 0x00], [0x01, 0x00, 0x00, 0x00], [0x02, 0x00, 0x00, 0x00], [0x04, 0x00, 0x00, 0x00], [0x08, 0x00, 0x00, 0x00], [0x10, 0x00, 0x00, 0x00], [0x20, 0x00, 0x00, 0x00], [0x40, 0x00, 0x00, 0x00], [0x80, 0x00, 0x00, 0x00], [0x1b, 0x00, 0x00, 0x00], [0x36, 0x00, 0x00, 0x00]];

    /**
     * AesCtr: Counter-mode (CTR) wrapper for AES.
     *
     * This encrypts a Unicode string to produces a base64 ciphertext using 128/192/256-bit AES,
     * and the converse to decrypt an encrypted ciphertext.
     *
     * See csrc.nist.gov/publications/detail/sp/800-38a/final
     */

    var AesCtr = function (_Aes) {
        _inherits(AesCtr, _Aes);

        function AesCtr() {
            _classCallCheck(this, AesCtr);

            return _possibleConstructorReturn(this, (AesCtr.__proto__ || Object.getPrototypeOf(AesCtr)).apply(this, arguments));
        }

        _createClass(AesCtr, null, [{
            key: 'encrypt',


            /**
             * Encrypt a text using AES encryption in Counter mode of operation.
             *
             * Unicode multi-byte character safe.
             *
             * @param   {string} plaintext - Source text to be encrypted.
             * @param   {string} password - The password to use to generate a key for encryption.
             * @param   {number} nBits - Number of bits to be used in the key; 128 / 192 / 256.
             * @returns {string} Encrypted text, base-64 encoded.
             *
             * @example
             *   const encr = AesCtr.encrypt('big secret', 'pāşšŵōřđ', 256); // 'lwGl66VVwVObKIr6of8HVqJr'
             */
            value: function encrypt(plaintext, password, nBits) {
                if (![128, 192, 256].includes(nBits)) throw new Error('Key size is not 128 / 192 / 256');
                plaintext = AesCtr.utf8Encode(String(plaintext));
                password = AesCtr.utf8Encode(String(password));

                // use AES itself to encrypt password to get cipher key (using plain password as source for key
                // expansion) to give us well encrypted key (in real use hashed password could be used for key)
                var nBytes = nBits / 8; // no bytes in key (16/24/32)
                var pwBytes = new Array(nBytes);
                for (var i = 0; i < nBytes; i++) {
                    // use 1st 16/24/32 chars of password for key
                    pwBytes[i] = i < password.length ? password.charCodeAt(i) : 0;
                }
                var key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes)); // gives us 16-byte key
                key = key.concat(key.slice(0, nBytes - 16)); // expand key to 16/24/32 bytes long

                // initialise 1st 8 bytes of counter block with nonce (NIST SP 800-38A §B.2): [0-1] = millisec,
                // [2-3] = random, [4-7] = seconds, together giving full sub-millisec uniqueness up to Feb 2106
                var timestamp = new Date().getTime(); // milliseconds since 1-Jan-1970
                var nonceMs = timestamp % 1000;
                var nonceSec = Math.floor(timestamp / 1000);
                var nonceRnd = Math.floor(Math.random() * 0xffff);
                // for debugging: const [ nonceMs, nonceSec, nonceRnd ] = [ 0, 0, 0 ];
                var counterBlock = [// 16-byte array; blocksize is fixed at 16 for AES
                    nonceMs & 0xff, nonceMs >>> 8 & 0xff, nonceRnd & 0xff, nonceRnd >>> 8 & 0xff, nonceSec & 0xff, nonceSec >>> 8 & 0xff, nonceSec >>> 16 & 0xff, nonceSec >>> 24 & 0xff, 0, 0, 0, 0, 0, 0, 0, 0];

                // and convert nonce to a string to go on the front of the ciphertext
                var nonceStr = counterBlock.slice(0, 8).map(function (i) {
                    return String.fromCharCode(i);
                }).join('');

                // convert (utf-8) plaintext to byte array
                var plaintextBytes = plaintext.split('').map(function (ch) {
                    return ch.charCodeAt(0);
                });

                // ------------ perform encryption ------------
                var ciphertextBytes = AesCtr.nistEncryption(plaintextBytes, key, counterBlock);

                // convert byte array to (utf-8) ciphertext string
                var ciphertextUtf8 = ciphertextBytes.map(function (i) {
                    return String.fromCharCode(i);
                }).join('');

                // base-64 encode ciphertext
                var ciphertextB64 = AesCtr.base64Encode(nonceStr + ciphertextUtf8);

                return ciphertextB64;
            }

            /**
             * NIST SP 800-38A sets out recommendations for block cipher modes of operation in terms of byte
             * operations. This implements the §6.5 Counter Mode (CTR).
             *
             *     Oⱼ = CIPHₖ(Tⱼ)      for j = 1, 2 … n
             *     Cⱼ = Pⱼ ⊕ Oⱼ        for j = 1, 2 … n-1
             *     C*ₙ = P* ⊕ MSBᵤ(Oₙ) final (partial?) block
             *   where CIPHₖ is the forward cipher function, O output blocks, P plaintext blocks, C
             *   ciphertext blocks
             *
             * @param   {number[]} plaintext - Plaintext to be encrypted, as byte array.
             * @param   {number[]} key - Key to be used to encrypt plaintext.
             * @param   {number[]} counterBlock - Initial 16-byte CTR counter block (with nonce & 0 counter).
             * @returns {number[]} Ciphertext as byte array.
             *
             * @private
             */

        }, {
            key: 'nistEncryption',
            value: function nistEncryption(plaintext, key, counterBlock) {
                var blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES

                // generate key schedule - an expansion of the key into distinct Key Rounds for each round
                var keySchedule = Aes.keyExpansion(key);

                var blockCount = Math.ceil(plaintext.length / blockSize);
                var ciphertext = new Array(plaintext.length);

                for (var b = 0; b < blockCount; b++) {
                    // ---- encrypt counter block; Oⱼ = CIPHₖ(Tⱼ) ----
                    var cipherCntr = Aes.cipher(counterBlock, keySchedule);

                    // block size is reduced on final block
                    var blockLength = b < blockCount - 1 ? blockSize : (plaintext.length - 1) % blockSize + 1;

                    // ---- xor plaintext with ciphered counter byte-by-byte; Cⱼ = Pⱼ ⊕ Oⱼ ----
                    for (var i = 0; i < blockLength; i++) {
                        ciphertext[b * blockSize + i] = cipherCntr[i] ^ plaintext[b * blockSize + i];
                    }

                    // increment counter block (counter in 2nd 8 bytes of counter block, big-endian)
                    counterBlock[blockSize - 1]++;
                    // and propagate carry digits
                    for (var _i3 = blockSize - 1; _i3 >= 8; _i3--) {
                        counterBlock[_i3 - 1] += counterBlock[_i3] >> 8;
                        counterBlock[_i3] &= 0xff;
                    }

                    // if within web worker, announce progress every 1000 blocks (roughly every 50ms)
                    if (typeof WorkerGlobalScope != 'undefined' && self instanceof WorkerGlobalScope) {
                        if (b % 1000 == 0) self.postMessage({ progress: b / blockCount });
                    }
                }

                return ciphertext;
            }

            /**
             * Decrypt a text encrypted by AES in counter mode of operation.
             *
             * @param   {string} ciphertext - Cipher text to be decrypted.
             * @param   {string} password - Password to use to generate a key for decryption.
             * @param   {number} nBits - Number of bits to be used in the key; 128 / 192 / 256.
             * @returns {string} Decrypted text
             *
             * @example
             *   const decr = AesCtr.decrypt('lwGl66VVwVObKIr6of8HVqJr', 'pāşšŵōřđ', 256); // 'big secret'
             */

        }, {
            key: 'decrypt',
            value: function decrypt(ciphertext, password, nBits) {
                if (![128, 192, 256].includes(nBits)) throw new Error('Key size is not 128 / 192 / 256');
                ciphertext = AesCtr.base64Decode(String(ciphertext));
                password = AesCtr.utf8Encode(String(password));

                // use AES to encrypt password (mirroring encrypt routine)
                var nBytes = nBits / 8; // no bytes in key
                var pwBytes = new Array(nBytes);
                for (var i = 0; i < nBytes; i++) {
                    // use 1st nBytes chars of password for key
                    pwBytes[i] = i < password.length ? password.charCodeAt(i) : 0;
                }
                var key = Aes.cipher(pwBytes, Aes.keyExpansion(pwBytes));
                key = key.concat(key.slice(0, nBytes - 16)); // expand key to 16/24/32 bytes long

                // recover nonce from 1st 8 bytes of ciphertext into 1st 8 bytes of counter block
                var counterBlock = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
                for (var _i4 = 0; _i4 < 8; _i4++) {
                    counterBlock[_i4] = ciphertext.charCodeAt(_i4);
                } // convert ciphertext to byte array (skipping past initial 8 bytes)
                var ciphertextBytes = new Array(ciphertext.length - 8);
                for (var _i5 = 8; _i5 < ciphertext.length; _i5++) {
                    ciphertextBytes[_i5 - 8] = ciphertext.charCodeAt(_i5);
                } // ------------ perform decryption ------------
                var plaintextBytes = AesCtr.nistDecryption(ciphertextBytes, key, counterBlock);

                // convert byte array to (utf-8) plaintext string
                var plaintextUtf8 = plaintextBytes.map(function (i) {
                    return String.fromCharCode(i);
                }).join('');

                // decode from UTF8 back to Unicode multi-byte chars
                var plaintext = AesCtr.utf8Decode(plaintextUtf8);

                return plaintext;
            }

            /**
             * NIST SP 800-38A sets out recommendations for block cipher modes of operation in terms of byte
             * operations. This implements the §6.5 Counter Mode (CTR).
             *
             *     Oⱼ = CIPHₖ(Tⱼ)      for j = 1, 2 … n
             *     Pⱼ = Cⱼ ⊕ Oⱼ        for j = 1, 2 … n-1
             *     P*ₙ = C* ⊕ MSBᵤ(Oₙ) final (partial?) block
             *   where CIPHₖ is the forward cipher function, O output blocks, C ciphertext blocks, P
             *   plaintext blocks
             *
             * @param   {number[]} ciphertext - Ciphertext to be decrypted, as byte array.
             * @param   {number[]} key - Key to be used to decrypt ciphertext.
             * @param   {number[]} counterBlock - Initial 16-byte CTR counter block (with nonce & 0 counter).
             * @returns {number[]} Plaintext as byte array.
             *
             * @private
             */

        }, {
            key: 'nistDecryption',
            value: function nistDecryption(ciphertext, key, counterBlock) {
                var blockSize = 16; // block size fixed at 16 bytes / 128 bits (Nb=4) for AES

                // generate key schedule - an expansion of the key into distinct Key Rounds for each round
                var keySchedule = Aes.keyExpansion(key);

                var blockCount = Math.ceil(ciphertext.length / blockSize);
                var plaintext = new Array(ciphertext.length);

                for (var b = 0; b < blockCount; b++) {
                    // ---- decrypt counter block; Oⱼ = CIPHₖ(Tⱼ) ----
                    var cipherCntr = Aes.cipher(counterBlock, keySchedule);

                    // block size is reduced on final block
                    var blockLength = b < blockCount - 1 ? blockSize : (ciphertext.length - 1) % blockSize + 1;

                    // ---- xor ciphertext with ciphered counter byte-by-byte; Pⱼ = Cⱼ ⊕ Oⱼ ----
                    for (var i = 0; i < blockLength; i++) {
                        plaintext[b * blockSize + i] = cipherCntr[i] ^ ciphertext[b * blockSize + i];
                    }

                    // increment counter block (counter in 2nd 8 bytes of counter block, big-endian)
                    counterBlock[blockSize - 1]++;
                    // and propagate carry digits
                    for (var _i6 = blockSize - 1; _i6 >= 8; _i6--) {
                        counterBlock[_i6 - 1] += counterBlock[_i6] >> 8;
                        counterBlock[_i6] &= 0xff;
                    }

                    // if within web worker, announce progress every 1000 blocks (roughly every 50ms)
                    if (typeof WorkerGlobalScope != 'undefined' && self instanceof WorkerGlobalScope) {
                        if (b % 1000 == 0) self.postMessage({ progress: b / blockCount });
                    }
                }

                return plaintext;
            }

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

            /**
             * Encodes multi-byte string to utf8.
             *
             * Note utf8Encode is an identity function with 7-bit ascii strings, but not with 8-bit strings;
             * utf8Encode('x') = 'x', but utf8Encode('ça') = 'Ã§a', and utf8Encode('Ã§a') = 'ÃÂ§a'.
             */

        }, {
            key: 'utf8Encode',
            value: function utf8Encode(str) {
                try {
                    return new TextEncoder().encode(str, 'utf-8').reduce(function (prev, curr) {
                        return prev + String.fromCharCode(curr);
                    }, '');
                } catch (e) {
                    // no TextEncoder available?
                    return unescape(encodeURIComponent(str)); // monsur.hossa.in/2012/07/20/utf-8-in-javascript.html
                }
            }

            /**
             * Decodes utf8 string to multi-byte.
             */

        }, {
            key: 'utf8Decode',
            value: function utf8Decode(str) {
                try {
                    return new TextEncoder().decode(str, 'utf-8').reduce(function (prev, curr) {
                        return prev + String.fromCharCode(curr);
                    }, '');
                } catch (e) {
                    // no TextEncoder available?
                    return decodeURIComponent(escape(str)); // monsur.hossa.in/2012/07/20/utf-8-in-javascript.html
                }
            }

            /*
             * Encodes string as base-64.
             *
             * - developer.mozilla.org/en-US/docs/Web/API/window.btoa, nodejs.org/api/buffer.html
             * - note: btoa & Buffer/binary work on single-byte Unicode (C0/C1), so ok for utf8 strings, not for general Unicode...
             * - note: if btoa()/atob() are not available (eg IE9-), try github.com/davidchambers/Base64.js
             */

        }, {
            key: 'base64Encode',
            value: function base64Encode(str) {
                if (typeof btoa != 'undefined') return btoa(str); // browser
                // if (typeof Buffer != 'undefined') return new Buffer(str, 'binary').toString('base64'); // Node.js
                if (typeof Buffer != 'undefined') return Buffer.from(str, 'binary').toString('base64'); // Node.js
                throw new Error('No Base64 Encode');
            }

            /*
             * Decodes base-64 encoded string.
             */

        }, {
            key: 'base64Decode',
            value: function base64Decode(str) {
                if (typeof atob != 'undefined') return atob(str); // browser
                // if (typeof Buffer != 'undefined') return new Buffer(str, 'base64').toString('binary'); // Node.js
                if (typeof Buffer != 'undefined') return Buffer.from(str, 'base64').toString('binary'); // Node.js
                throw new Error('No Base64 Decode');
            }
        }]);

        return AesCtr;
    }(Aes);

    exports.__aesCTR = AesCtr;
    exports.__aes    = Aes;
})(window);


// The MIT License (MIT)
//
// Copyright (c) 2014-2019 Eric J Nesser
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
//     The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
//     THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
//     FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
//     OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
//

/********************************************************************************
 vCards-js, Eric J Nesser, November 2014
 ********************************************************************************/
/**
 * Represents a contact that can be imported into Outlook, iOS, Mac OS, Android devices, and more
 */

var __vCard = (function () {
    'use strict';

    var vCardFormatter = (function () {
        var majorVersion = '3';

        /**
         * Encode string
         * @param  {String}     value to encode
         * @return {String}     encoded string
         */
        function e(value) {
            if (value) {
                if (typeof(value) !== 'string') {
                    value = '' + value;
                }
                return value.replace(/\n/g, '\\n').replace(/,/g, '\\,').replace(/;/g, '\\;');
            }
            return '';
        }

        /**
         * Return new line characters
         * @return {String} new line characters
         */
        function nl() {
            return '\r\n';
        }

        /**
         * Get formatted photo
         * @param  {String} photoType       Photo type (PHOTO, LOGO)
         * @param  {String} url             URL to attach photo from
         * @param  {String} mediaType       Media-type of photo (JPEG, PNG, GIF)
         * @return {String}                 Formatted photo
         */
        function getFormattedPhoto(photoType, url, mediaType, base64) {

            var params;

            if (majorVersion >= 4) {
                params = base64 ? ';ENCODING=b;MEDIATYPE=image/' : ';MEDIATYPE=image/';
            } else if (majorVersion === 3) {
                params = base64 ? ';ENCODING=b;TYPE=' : ';TYPE=';
            } else {
                params = base64 ? ';ENCODING=BASE64;' : ';';
            }

            var formattedPhoto = photoType + params + mediaType + ':' + e(url) + nl();
            return formattedPhoto;
        }

        /**
         * Get formatted address
         * @param  {object}         address
         * @param  {object}         encoding prefix
         * @return {String}         Formatted address
         */
        function getFormattedAddress(encodingPrefix, address) {

            var formattedAddress = '';

            if (address.details.label ||
                address.details.street ||
                address.details.city ||
                address.details.stateProvince ||
                address.details.postalCode ||
                address.details.countryRegion) {

                if (majorVersion >= 4) {
                    formattedAddress = 'ADR' + encodingPrefix + ';TYPE=' + address.type +
                        (address.details.label ? ';LABEL="' + e(address.details.label) + '"' : '') + ':;;' +
                        e(address.details.street) + ';' +
                        e(address.details.city) + ';' +
                        e(address.details.stateProvince) + ';' +
                        e(address.details.postalCode) + ';' +
                        e(address.details.countryRegion) + nl();
                } else {
                    if (address.details.label) {
                        formattedAddress = 'LABEL' + encodingPrefix + ';TYPE=' + address.type + ':' + e(address.details.label) + nl();
                    }
                    formattedAddress += 'ADR' + encodingPrefix + ';TYPE=' + address.type + ':;;' +
                        e(address.details.street) + ';' +
                        e(address.details.city) + ';' +
                        e(address.details.stateProvince) + ';' +
                        e(address.details.postalCode) + ';' +
                        e(address.details.countryRegion) + nl();

                }
            }

            return formattedAddress;
        }

        /**
         * Convert date to YYYYMMDD format
         * @param  {Date}       date to encode
         * @return {String}     encoded date
         */
        function YYYYMMDD(date) {
            return date.getFullYear() + ('0' + (date.getMonth()+1)).slice(-2) + ('0' + date.getDate()).slice(-2);
        }

        return {

            /**
             * Get formatted vCard in VCF format
             * @param  {object}     vCard object
             * @return {String}     Formatted vCard in VCF format
             */
            getFormattedString: function(vCard) {

                majorVersion = vCard.getMajorVersion();

                var formattedVCardString = '';
                formattedVCardString += 'BEGIN:VCARD' + nl();
                formattedVCardString += 'VERSION:' + vCard.version + nl();

                var encodingPrefix = majorVersion >= 4 ? '' : ';CHARSET=UTF-8';
                var formattedName = vCard.formattedName;

                if (!formattedName) {
                    formattedName = '';

                    [vCard.firstName, vCard.middleName, vCard.lastName]
                    .forEach(function(name) {
                        if (name) {
                            if (formattedName) {
                                formattedName += ' ';
                            }
                        }
                        formattedName += name;
                    });
                }

                formattedVCardString += 'FN' + encodingPrefix + ':' + e(formattedName) + nl();
                formattedVCardString += 'N' + encodingPrefix + ':' +
                    e(vCard.lastName) + ';' +
                    e(vCard.firstName) + ';' +
                    e(vCard.middleName) + ';' +
                    e(vCard.namePrefix) + ';' +
                    e(vCard.nameSuffix) + nl();

                if (vCard.nickname && majorVersion >= 3) {
                    formattedVCardString += 'NICKNAME' + encodingPrefix + ':' + e(vCard.nickname) + nl();
                }

                if (vCard.gender) {
                    formattedVCardString += 'GENDER:' + e(vCard.gender) + nl();
                }

                if (vCard.uid) {
                    formattedVCardString += 'UID' + encodingPrefix + ':' + e(vCard.uid) + nl();
                }

                if (vCard.birthday) {
                    formattedVCardString += 'BDAY:' + YYYYMMDD(vCard.birthday) + nl();
                }

                if (vCard.anniversary) {
                    formattedVCardString += 'ANNIVERSARY:' + YYYYMMDD(vCard.anniversary) + nl();
                }

                if (vCard.email) {
                    if(!Array.isArray(vCard.email)){
                        vCard.email = [vCard.email];
                    }
                    vCard.email.forEach(
                        function(address) {
                            if (majorVersion >= 4) {
                                formattedVCardString += 'EMAIL' + encodingPrefix + ';type=HOME:' + e(address) + nl();
                            } else if (majorVersion >= 3 && majorVersion < 4) {
                                formattedVCardString += 'EMAIL' + encodingPrefix + ';type=HOME,INTERNET:' + e(address) + nl();
                            } else {
                                formattedVCardString += 'EMAIL' + encodingPrefix + ';HOME;INTERNET:' + e(address) + nl();
                            }
                        }
                    );
                }

                if (vCard.workEmail) {
                    if(!Array.isArray(vCard.workEmail)){
                        vCard.workEmail = [vCard.workEmail];
                    }
                    vCard.workEmail.forEach(
                        function(address) {
                            if (majorVersion >= 4) {
                                formattedVCardString += 'EMAIL' + encodingPrefix + ';type=WORK:' + e(address) + nl();
                            } else if (majorVersion >= 3 && majorVersion < 4) {
                                formattedVCardString += 'EMAIL' + encodingPrefix + ';type=WORK,INTERNET:' + e(address) + nl();
                            } else {
                                formattedVCardString += 'EMAIL' + encodingPrefix + ';WORK;INTERNET:' + e(address) + nl();
                            }
                        }
                    );
                }

                if (vCard.otherEmail) {
                    if(!Array.isArray(vCard.otherEmail)){
                        vCard.otherEmail = [vCard.otherEmail];
                    }
                    vCard.otherEmail.forEach(
                        function(address) {
                            if (majorVersion >= 4) {
                                formattedVCardString += 'EMAIL' + encodingPrefix + ';type=OTHER:' + e(address) + nl();
                            } else if (majorVersion >= 3 && majorVersion < 4) {
                                formattedVCardString += 'EMAIL' + encodingPrefix + ';type=OTHER,INTERNET:' + e(address) + nl();
                            } else {
                                formattedVCardString += 'EMAIL' + encodingPrefix + ';OTHER;INTERNET:' + e(address) + nl();
                            }
                        }
                    );
                }

                if (vCard.logo.url) {
                    formattedVCardString += getFormattedPhoto('LOGO', vCard.logo.url, vCard.logo.mediaType, vCard.logo.base64);
                }

                if (vCard.photo.url) {
                    formattedVCardString += getFormattedPhoto('PHOTO', vCard.photo.url, vCard.photo.mediaType, vCard.photo.base64);
                }

                if (vCard.cellPhone) {
                    if(!Array.isArray(vCard.cellPhone)){
                        vCard.cellPhone = [vCard.cellPhone];
                    }
                    vCard.cellPhone.forEach(
                        function(number){
                            if (majorVersion >= 4) {
                                formattedVCardString += 'TEL;VALUE=uri;TYPE="voice,cell":tel:' + e(number) + nl();
                            } else {
                                formattedVCardString += 'TEL;TYPE=CELL:' + e(number) + nl();
                            }
                        }
                    );
                }

                if (vCard.pagerPhone) {
                    if(!Array.isArray(vCard.pagerPhone)){
                        vCard.pagerPhone = [vCard.pagerPhone];
                    }
                    vCard.pagerPhone.forEach(
                        function(number) {
                            if (majorVersion >= 4) {
                                formattedVCardString += 'TEL;VALUE=uri;TYPE="pager,cell":tel:' + e(number) + nl();
                            } else {
                                formattedVCardString += 'TEL;TYPE=PAGER:' + e(number) + nl();
                            }
                        }
                    );
                }

                if (vCard.homePhone) {
                    if(!Array.isArray(vCard.homePhone)){
                        vCard.homePhone = [vCard.homePhone];
                    }
                    vCard.homePhone.forEach(
                        function(number) {
                            if (majorVersion >= 4) {
                                formattedVCardString += 'TEL;VALUE=uri;TYPE="voice,home":tel:' + e(number) + nl();
                            } else {
                                formattedVCardString += 'TEL;TYPE=HOME,VOICE:' + e(number) + nl();
                            }
                        }
                    );
                }

                if (vCard.workPhone) {
                    if(!Array.isArray(vCard.workPhone)){
                        vCard.workPhone = [vCard.workPhone];
                    }
                    vCard.workPhone.forEach(
                        function(number) {
                            if (majorVersion >= 4) {
                                formattedVCardString += 'TEL;VALUE=uri;TYPE="voice,work":tel:' + e(number) + nl();

                            } else {
                                formattedVCardString += 'TEL;TYPE=WORK,VOICE:' + e(number) + nl();
                            }
                        }
                    );
                }

                if (vCard.homeFax) {
                    if(!Array.isArray(vCard.homeFax)){
                        vCard.homeFax = [vCard.homeFax];
                    }
                    vCard.homeFax.forEach(
                        function(number) {
                            if (majorVersion >= 4) {
                                formattedVCardString += 'TEL;VALUE=uri;TYPE="fax,home":tel:' + e(number) + nl();

                            } else {
                                formattedVCardString += 'TEL;TYPE=HOME,FAX:' + e(number) + nl();
                            }
                        }
                    );
                }

                if (vCard.workFax) {
                    if(!Array.isArray(vCard.workFax)){
                        vCard.workFax = [vCard.workFax];
                    }
                    vCard.workFax.forEach(
                        function(number) {
                            if (majorVersion >= 4) {
                                formattedVCardString += 'TEL;VALUE=uri;TYPE="fax,work":tel:' + e(number) + nl();

                            } else {
                                formattedVCardString += 'TEL;TYPE=WORK,FAX:' + e(number) + nl();
                            }
                        }
                    );
                }

                if (vCard.otherPhone) {
                    if(!Array.isArray(vCard.otherPhone)){
                        vCard.otherPhone = [vCard.otherPhone];
                    }
                    vCard.otherPhone.forEach(
                        function(number) {
                            if (majorVersion >= 4) {
                                formattedVCardString += 'TEL;VALUE=uri;TYPE="voice,other":tel:' + e(number) + nl();

                            } else {
                                formattedVCardString += 'TEL;TYPE=OTHER:' + e(number) + nl();
                            }
                        }
                    );
                }

                [{
                    details: vCard.homeAddress,
                    type: 'HOME'
                }, {
                    details: vCard.workAddress,
                    type: 'WORK'
                }].forEach(
                    function(address) {
                        formattedVCardString += getFormattedAddress(encodingPrefix, address);
                    }
                );

                if (vCard.title) {
                    formattedVCardString += 'TITLE' + encodingPrefix + ':' + e(vCard.title) + nl();
                }

                if (vCard.role) {
                    formattedVCardString += 'ROLE' + encodingPrefix + ':' + e(vCard.role) + nl();
                }

                if (vCard.organization) {
                    formattedVCardString += 'ORG' + encodingPrefix + ':' + e(vCard.organization) + nl();
                }

                if (vCard.url) {
                    formattedVCardString += 'URL' + encodingPrefix + ':' + e(vCard.url) + nl();
                }

                if (vCard.workUrl) {
                    formattedVCardString += 'URL;type=WORK' + encodingPrefix + ':' + e(vCard.workUrl) + nl();
                }

                if (vCard.note) {
                    formattedVCardString += 'NOTE' + encodingPrefix + ':' + e(vCard.note) + nl();
                }

                if (vCard.socialUrls) {
                    for (var key in vCard.socialUrls) {
                        if (vCard.socialUrls.hasOwnProperty(key) &&
                            vCard.socialUrls[key]) {
                            formattedVCardString += 'X-SOCIALPROFILE;TYPE=' + key + ':' + e(vCard.socialUrls[key]) + nl();
                        }
                    }
                }

                if (vCard.source) {
                    formattedVCardString += 'SOURCE' + encodingPrefix + ':' + e(vCard.source) + nl();
                }

                formattedVCardString += 'REV:' + (new Date()).toISOString() + nl();

                if (vCard.isOrganization) {
                    formattedVCardString += 'X-ABShowAs:COMPANY' + nl();
                }

                formattedVCardString += 'END:VCARD' + nl();
                return formattedVCardString;
            }
        };
    })();

    /**
     * Get photo object for storing photos in vCards
     */
    function getPhoto() {
        return {
            url: '',
            mediaType: '',
            base64: false,

            /**
             * Attach a photo from a URL
             * @param  {string} url       URL where photo can be found
             * @param  {string} mediaType Media type of photo (JPEG, PNG, GIF)
             */
            attachFromUrl: function(url, mediaType) {
                this.url = url;
                this.mediaType = mediaType;
                this.base64 = false;
            },

            /**
             * Embed a photo from a file using base-64 encoding (not implemented yet)
             * @param  {string} filename
             */
            embedFromFile: function(fileLocation) {
                var fs   = require('fs');
                var path = require('path');
                this.mediaType = path.extname(fileLocation).toUpperCase().replace(/\./g, "");
                var imgData = fs.readFileSync(fileLocation);
                this.url = imgData.toString('base64');
                this.base64 = true;
            },

            /**
             * Embed a photo from a base-64 string
             * @param  {string} base64String
             */
            embedFromString: function(base64String, mediaType) {
                this.mediaType = mediaType;
                this.url = base64String;
                this.base64 = true;
            }
        };
    }

    /**
     * Get a mailing address to attach to a vCard.
     */
    function getMailingAddress() {
        return {
            /**
             * Represents the actual text that should be put on the mailing label when delivering a physical package
             * @type {String}
             */
            label: '',

            /**
             * Street address
             * @type {String}
             */
            street: '',

            /**
             * City
             * @type {String}
             */
            city: '',

            /**
             * State or province
             * @type {String}
             */
            stateProvince: '',

            /**
             * Postal code
             * @type {String}
             */
            postalCode: '',

            /**
             * Country or region
             * @type {String}
             */
            countryRegion: ''
        };
    }

    /**
     * Get social media URLs
     * @return {object} Social media URL hash group
     */
    function getSocialUrls() {
        return {
            'facebook': '',
            'linkedIn': '',
            'twitter': '',
            'flickr': ''
        };
    }

    /********************************************************************************
     * Public interface for vCard
     ********************************************************************************/
    return {

        /**
         * Specifies a value that represents a persistent, globally unique identifier associated with the vCard
         * @type {String}
         */
        uid: '',

        /**
         * Date of birth
         * @type {Datetime}
         */
        birthday: '',

        /**
         * Cell phone number
         * @type {String}
         */
        cellPhone: '',

        /**
         * Other cell phone number or pager
         * @type {String}
         */
        pagerPhone: '',

        /**
         * The address for private electronic mail communication
         * @type {String}
         */
        email: '',

        /**
         * The address for work-related electronic mail communication
         * @type {String}
         */
        workEmail: '',

        /**
         * First name
         * @type {String}
         */
        firstName: '',

        /**
         * Formatted name string associated with the vCard object (will automatically populate if not set)
         * @type {String}
         */
        formattedName: '',

        /**
         * Gender.
         * @type {String} Must be M or F for Male or Female
         */
        gender: '',

        /**
         * Home mailing address
         * @type {object}
         */
        homeAddress: getMailingAddress(),

        /**
         * Home phone
         * @type {String}
         */
        homePhone: '',

        /**
         * Home facsimile
         * @type {String}
         */
        homeFax: '',

        /**
         * Last name
         * @type {String}
         */
        lastName: '',

        /**
         * Logo
         * @type {object}
         */
        logo: getPhoto(),

        /**
         * Middle name
         * @type {String}
         */
        middleName: '',

        /**
         * Prefix for individual's name
         * @type {String}
         */
        namePrefix: '',

        /**
         * Suffix for individual's name
         * @type {String}
         */
        nameSuffix: '',

        /**
         * Nickname of individual
         * @type {String}
         */
        nickname: '',

        /**
         * Specifies supplemental information or a comment that is associated with the vCard
         * @type {String}
         */
        note: '',

        /**
         * The name and optionally the unit(s) of the organization associated with the vCard object
         * @type {String}
         */
        organization: '',

        /**
         * Individual's photo
         * @type {object}
         */
        photo: getPhoto(),

        /**
         * The role, occupation, or business category of the vCard object within an organization
         * @type {String}
         */
        role: '',

        /**
         * Social URLs attached to the vCard object (ex: Facebook, Twitter, LinkedIn)
         * @type {String}
         */
        socialUrls: getSocialUrls(),

        /**
         * A URL that can be used to get the latest version of this vCard
         * @type {String}
         */
        source: '',

        /**
         * Specifies the job title, functional position or function of the individual within an organization
         * @type {String}
         */
        title: '',

        /**
         * URL pointing to a website that represents the person in some way
         * @type {String}
         */
        url: '',

        /**
         * URL pointing to a website that represents the person's work in some way
         * @type {String}
         */
        workUrl: '',

        /**
         * Work mailing address
         * @type {object}
         */
        workAddress: getMailingAddress(),

        /**
         * Work phone
         * @type {String}
         */
        workPhone: '',

        /**
         * Work facsimile
         * @type {String}
         */
        workFax: '',

        /**
         * vCard version
         * @type {String}
         */
        version: '3.0',

        /**
         * Get major version of the vCard format
         * @return {integer}
         */
        getMajorVersion: function() {
            var majorVersionString = this.version ? this.version.split('.')[0] : '4';
            if (!isNaN(majorVersionString)) {
                return parseInt(majorVersionString);
            }
            return 4;
        },

        /**
         * Get formatted vCard
         * @return {String} Formatted vCard in VCF format
         */
        getFormattedString: function() {
            //var vCardFormatter = require('./lib/vCardFormatter');
            return vCardFormatter.getFormattedString(this);
        },

        /**
         * Save formatted vCard to file
         * @param  {String} filename
         */
        saveToFile: function(filename) {
            //var vCardFormatter = require('./lib/vCardFormatter');
            var contents = vCardFormatter.getFormattedString(this);

            var fs = require('fs');
            fs.writeFileSync(filename, contents, { encoding: 'utf8' });
        }
    };
});


var QRCode;!function(){function a(a){this.mode=c.MODE_8BIT_BYTE,this.data=a,this.parsedData=[];for(var b=[],d=0,e=this.data.length;e>d;d++){var f=this.data.charCodeAt(d);f>65536?(b[0]=240|(1835008&f)>>>18,b[1]=128|(258048&f)>>>12,b[2]=128|(4032&f)>>>6,b[3]=128|63&f):f>2048?(b[0]=224|(61440&f)>>>12,b[1]=128|(4032&f)>>>6,b[2]=128|63&f):f>128?(b[0]=192|(1984&f)>>>6,b[1]=128|63&f):b[0]=f,this.parsedData=this.parsedData.concat(b)}this.parsedData.length!=this.data.length&&(this.parsedData.unshift(191),this.parsedData.unshift(187),this.parsedData.unshift(239))}function b(a,b){this.typeNumber=a,this.errorCorrectLevel=b,this.modules=null,this.moduleCount=0,this.dataCache=null,this.dataList=[]}function i(a,b){if(void 0==a.length)throw new Error(a.length+"/"+b);for(var c=0;c<a.length&&0==a[c];)c++;this.num=new Array(a.length-c+b);for(var d=0;d<a.length-c;d++)this.num[d]=a[d+c]}function j(a,b){this.totalCount=a,this.dataCount=b}function k(){this.buffer=[],this.length=0}function m(){return"undefined"!=typeof CanvasRenderingContext2D}function n(){var a=!1,b=navigator.userAgent;return/android/i.test(b)&&(a=!0,aMat=b.toString().match(/android ([0-9]\.[0-9])/i),aMat&&aMat[1]&&(a=parseFloat(aMat[1]))),a}function r(a,b){for(var c=1,e=s(a),f=0,g=l.length;g>=f;f++){var h=0;switch(b){case d.L:h=l[f][0];break;case d.M:h=l[f][1];break;case d.Q:h=l[f][2];break;case d.H:h=l[f][3]}if(h>=e)break;c++}if(c>l.length)throw new Error("Too long data");return c}function s(a){var b=encodeURI(a).toString().replace(/\%[0-9a-fA-F]{2}/g,"a");return b.length+(b.length!=a?3:0)}a.prototype={getLength:function(){return this.parsedData.length},write:function(a){for(var b=0,c=this.parsedData.length;c>b;b++)a.put(this.parsedData[b],8)}},b.prototype={addData:function(b){var c=new a(b);this.dataList.push(c),this.dataCache=null},isDark:function(a,b){if(0>a||this.moduleCount<=a||0>b||this.moduleCount<=b)throw new Error(a+","+b);return this.modules[a][b]},getModuleCount:function(){return this.moduleCount},make:function(){this.makeImpl(!1,this.getBestMaskPattern())},makeImpl:function(a,c){this.moduleCount=4*this.typeNumber+17,this.modules=new Array(this.moduleCount);for(var d=0;d<this.moduleCount;d++){this.modules[d]=new Array(this.moduleCount);for(var e=0;e<this.moduleCount;e++)this.modules[d][e]=null}this.setupPositionProbePattern(0,0),this.setupPositionProbePattern(this.moduleCount-7,0),this.setupPositionProbePattern(0,this.moduleCount-7),this.setupPositionAdjustPattern(),this.setupTimingPattern(),this.setupTypeInfo(a,c),this.typeNumber>=7&&this.setupTypeNumber(a),null==this.dataCache&&(this.dataCache=b.createData(this.typeNumber,this.errorCorrectLevel,this.dataList)),this.mapData(this.dataCache,c)},setupPositionProbePattern:function(a,b){for(var c=-1;7>=c;c++)if(!(-1>=a+c||this.moduleCount<=a+c))for(var d=-1;7>=d;d++)-1>=b+d||this.moduleCount<=b+d||(this.modules[a+c][b+d]=c>=0&&6>=c&&(0==d||6==d)||d>=0&&6>=d&&(0==c||6==c)||c>=2&&4>=c&&d>=2&&4>=d?!0:!1)},getBestMaskPattern:function(){for(var a=0,b=0,c=0;8>c;c++){this.makeImpl(!0,c);var d=f.getLostPoint(this);(0==c||a>d)&&(a=d,b=c)}return b},createMovieClip:function(a,b,c){var d=a.createEmptyMovieClip(b,c),e=1;this.make();for(var f=0;f<this.modules.length;f++)for(var g=f*e,h=0;h<this.modules[f].length;h++){var i=h*e,j=this.modules[f][h];j&&(d.beginFill(0,100),d.moveTo(i,g),d.lineTo(i+e,g),d.lineTo(i+e,g+e),d.lineTo(i,g+e),d.endFill())}return d},setupTimingPattern:function(){for(var a=8;a<this.moduleCount-8;a++)null==this.modules[a][6]&&(this.modules[a][6]=0==a%2);for(var b=8;b<this.moduleCount-8;b++)null==this.modules[6][b]&&(this.modules[6][b]=0==b%2)},setupPositionAdjustPattern:function(){for(var a=f.getPatternPosition(this.typeNumber),b=0;b<a.length;b++)for(var c=0;c<a.length;c++){var d=a[b],e=a[c];if(null==this.modules[d][e])for(var g=-2;2>=g;g++)for(var h=-2;2>=h;h++)this.modules[d+g][e+h]=-2==g||2==g||-2==h||2==h||0==g&&0==h?!0:!1}},setupTypeNumber:function(a){for(var b=f.getBCHTypeNumber(this.typeNumber),c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[Math.floor(c/3)][c%3+this.moduleCount-8-3]=d}for(var c=0;18>c;c++){var d=!a&&1==(1&b>>c);this.modules[c%3+this.moduleCount-8-3][Math.floor(c/3)]=d}},setupTypeInfo:function(a,b){for(var c=this.errorCorrectLevel<<3|b,d=f.getBCHTypeInfo(c),e=0;15>e;e++){var g=!a&&1==(1&d>>e);6>e?this.modules[e][8]=g:8>e?this.modules[e+1][8]=g:this.modules[this.moduleCount-15+e][8]=g}for(var e=0;15>e;e++){var g=!a&&1==(1&d>>e);8>e?this.modules[8][this.moduleCount-e-1]=g:9>e?this.modules[8][15-e-1+1]=g:this.modules[8][15-e-1]=g}this.modules[this.moduleCount-8][8]=!a},mapData:function(a,b){for(var c=-1,d=this.moduleCount-1,e=7,g=0,h=this.moduleCount-1;h>0;h-=2)for(6==h&&h--;;){for(var i=0;2>i;i++)if(null==this.modules[d][h-i]){var j=!1;g<a.length&&(j=1==(1&a[g]>>>e));var k=f.getMask(b,d,h-i);k&&(j=!j),this.modules[d][h-i]=j,e--,-1==e&&(g++,e=7)}if(d+=c,0>d||this.moduleCount<=d){d-=c,c=-c;break}}}},b.PAD0=236,b.PAD1=17,b.createData=function(a,c,d){for(var e=j.getRSBlocks(a,c),g=new k,h=0;h<d.length;h++){var i=d[h];g.put(i.mode,4),g.put(i.getLength(),f.getLengthInBits(i.mode,a)),i.write(g)}for(var l=0,h=0;h<e.length;h++)l+=e[h].dataCount;if(g.getLengthInBits()>8*l)throw new Error("code length overflow. ("+g.getLengthInBits()+">"+8*l+")");for(g.getLengthInBits()+4<=8*l&&g.put(0,4);0!=g.getLengthInBits()%8;)g.putBit(!1);for(;;){if(g.getLengthInBits()>=8*l)break;if(g.put(b.PAD0,8),g.getLengthInBits()>=8*l)break;g.put(b.PAD1,8)}return b.createBytes(g,e)},b.createBytes=function(a,b){for(var c=0,d=0,e=0,g=new Array(b.length),h=new Array(b.length),j=0;j<b.length;j++){var k=b[j].dataCount,l=b[j].totalCount-k;d=Math.max(d,k),e=Math.max(e,l),g[j]=new Array(k);for(var m=0;m<g[j].length;m++)g[j][m]=255&a.buffer[m+c];c+=k;var n=f.getErrorCorrectPolynomial(l),o=new i(g[j],n.getLength()-1),p=o.mod(n);h[j]=new Array(n.getLength()-1);for(var m=0;m<h[j].length;m++){var q=m+p.getLength()-h[j].length;h[j][m]=q>=0?p.get(q):0}}for(var r=0,m=0;m<b.length;m++)r+=b[m].totalCount;for(var s=new Array(r),t=0,m=0;d>m;m++)for(var j=0;j<b.length;j++)m<g[j].length&&(s[t++]=g[j][m]);for(var m=0;e>m;m++)for(var j=0;j<b.length;j++)m<h[j].length&&(s[t++]=h[j][m]);return s};for(var c={MODE_NUMBER:1,MODE_ALPHA_NUM:2,MODE_8BIT_BYTE:4,MODE_KANJI:8},d={L:1,M:0,Q:3,H:2},e={PATTERN000:0,PATTERN001:1,PATTERN010:2,PATTERN011:3,PATTERN100:4,PATTERN101:5,PATTERN110:6,PATTERN111:7},f={PATTERN_POSITION_TABLE:[[],[6,18],[6,22],[6,26],[6,30],[6,34],[6,22,38],[6,24,42],[6,26,46],[6,28,50],[6,30,54],[6,32,58],[6,34,62],[6,26,46,66],[6,26,48,70],[6,26,50,74],[6,30,54,78],[6,30,56,82],[6,30,58,86],[6,34,62,90],[6,28,50,72,94],[6,26,50,74,98],[6,30,54,78,102],[6,28,54,80,106],[6,32,58,84,110],[6,30,58,86,114],[6,34,62,90,118],[6,26,50,74,98,122],[6,30,54,78,102,126],[6,26,52,78,104,130],[6,30,56,82,108,134],[6,34,60,86,112,138],[6,30,58,86,114,142],[6,34,62,90,118,146],[6,30,54,78,102,126,150],[6,24,50,76,102,128,154],[6,28,54,80,106,132,158],[6,32,58,84,110,136,162],[6,26,54,82,110,138,166],[6,30,58,86,114,142,170]],G15:1335,G18:7973,G15_MASK:21522,getBCHTypeInfo:function(a){for(var b=a<<10;f.getBCHDigit(b)-f.getBCHDigit(f.G15)>=0;)b^=f.G15<<f.getBCHDigit(b)-f.getBCHDigit(f.G15);return(a<<10|b)^f.G15_MASK},getBCHTypeNumber:function(a){for(var b=a<<12;f.getBCHDigit(b)-f.getBCHDigit(f.G18)>=0;)b^=f.G18<<f.getBCHDigit(b)-f.getBCHDigit(f.G18);return a<<12|b},getBCHDigit:function(a){for(var b=0;0!=a;)b++,a>>>=1;return b},getPatternPosition:function(a){return f.PATTERN_POSITION_TABLE[a-1]},getMask:function(a,b,c){switch(a){case e.PATTERN000:return 0==(b+c)%2;case e.PATTERN001:return 0==b%2;case e.PATTERN010:return 0==c%3;case e.PATTERN011:return 0==(b+c)%3;case e.PATTERN100:return 0==(Math.floor(b/2)+Math.floor(c/3))%2;case e.PATTERN101:return 0==b*c%2+b*c%3;case e.PATTERN110:return 0==(b*c%2+b*c%3)%2;case e.PATTERN111:return 0==(b*c%3+(b+c)%2)%2;default:throw new Error("bad maskPattern:"+a)}},getErrorCorrectPolynomial:function(a){for(var b=new i([1],0),c=0;a>c;c++)b=b.multiply(new i([1,g.gexp(c)],0));return b},getLengthInBits:function(a,b){if(b>=1&&10>b)switch(a){case c.MODE_NUMBER:return 10;case c.MODE_ALPHA_NUM:return 9;case c.MODE_8BIT_BYTE:return 8;case c.MODE_KANJI:return 8;default:throw new Error("mode:"+a)}else if(27>b)switch(a){case c.MODE_NUMBER:return 12;case c.MODE_ALPHA_NUM:return 11;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 10;default:throw new Error("mode:"+a)}else{if(!(41>b))throw new Error("type:"+b);switch(a){case c.MODE_NUMBER:return 14;case c.MODE_ALPHA_NUM:return 13;case c.MODE_8BIT_BYTE:return 16;case c.MODE_KANJI:return 12;default:throw new Error("mode:"+a)}}},getLostPoint:function(a){for(var b=a.getModuleCount(),c=0,d=0;b>d;d++)for(var e=0;b>e;e++){for(var f=0,g=a.isDark(d,e),h=-1;1>=h;h++)if(!(0>d+h||d+h>=b))for(var i=-1;1>=i;i++)0>e+i||e+i>=b||(0!=h||0!=i)&&g==a.isDark(d+h,e+i)&&f++;f>5&&(c+=3+f-5)}for(var d=0;b-1>d;d++)for(var e=0;b-1>e;e++){var j=0;a.isDark(d,e)&&j++,a.isDark(d+1,e)&&j++,a.isDark(d,e+1)&&j++,a.isDark(d+1,e+1)&&j++,(0==j||4==j)&&(c+=3)}for(var d=0;b>d;d++)for(var e=0;b-6>e;e++)a.isDark(d,e)&&!a.isDark(d,e+1)&&a.isDark(d,e+2)&&a.isDark(d,e+3)&&a.isDark(d,e+4)&&!a.isDark(d,e+5)&&a.isDark(d,e+6)&&(c+=40);for(var e=0;b>e;e++)for(var d=0;b-6>d;d++)a.isDark(d,e)&&!a.isDark(d+1,e)&&a.isDark(d+2,e)&&a.isDark(d+3,e)&&a.isDark(d+4,e)&&!a.isDark(d+5,e)&&a.isDark(d+6,e)&&(c+=40);for(var k=0,e=0;b>e;e++)for(var d=0;b>d;d++)a.isDark(d,e)&&k++;var l=Math.abs(100*k/b/b-50)/5;return c+=10*l}},g={glog:function(a){if(1>a)throw new Error("glog("+a+")");return g.LOG_TABLE[a]},gexp:function(a){for(;0>a;)a+=255;for(;a>=256;)a-=255;return g.EXP_TABLE[a]},EXP_TABLE:new Array(256),LOG_TABLE:new Array(256)},h=0;8>h;h++)g.EXP_TABLE[h]=1<<h;for(var h=8;256>h;h++)g.EXP_TABLE[h]=g.EXP_TABLE[h-4]^g.EXP_TABLE[h-5]^g.EXP_TABLE[h-6]^g.EXP_TABLE[h-8];for(var h=0;255>h;h++)g.LOG_TABLE[g.EXP_TABLE[h]]=h;i.prototype={get:function(a){return this.num[a]},getLength:function(){return this.num.length},multiply:function(a){for(var b=new Array(this.getLength()+a.getLength()-1),c=0;c<this.getLength();c++)for(var d=0;d<a.getLength();d++)b[c+d]^=g.gexp(g.glog(this.get(c))+g.glog(a.get(d)));return new i(b,0)},mod:function(a){if(this.getLength()-a.getLength()<0)return this;for(var b=g.glog(this.get(0))-g.glog(a.get(0)),c=new Array(this.getLength()),d=0;d<this.getLength();d++)c[d]=this.get(d);for(var d=0;d<a.getLength();d++)c[d]^=g.gexp(g.glog(a.get(d))+b);return new i(c,0).mod(a)}},j.RS_BLOCK_TABLE=[[1,26,19],[1,26,16],[1,26,13],[1,26,9],[1,44,34],[1,44,28],[1,44,22],[1,44,16],[1,70,55],[1,70,44],[2,35,17],[2,35,13],[1,100,80],[2,50,32],[2,50,24],[4,25,9],[1,134,108],[2,67,43],[2,33,15,2,34,16],[2,33,11,2,34,12],[2,86,68],[4,43,27],[4,43,19],[4,43,15],[2,98,78],[4,49,31],[2,32,14,4,33,15],[4,39,13,1,40,14],[2,121,97],[2,60,38,2,61,39],[4,40,18,2,41,19],[4,40,14,2,41,15],[2,146,116],[3,58,36,2,59,37],[4,36,16,4,37,17],[4,36,12,4,37,13],[2,86,68,2,87,69],[4,69,43,1,70,44],[6,43,19,2,44,20],[6,43,15,2,44,16],[4,101,81],[1,80,50,4,81,51],[4,50,22,4,51,23],[3,36,12,8,37,13],[2,116,92,2,117,93],[6,58,36,2,59,37],[4,46,20,6,47,21],[7,42,14,4,43,15],[4,133,107],[8,59,37,1,60,38],[8,44,20,4,45,21],[12,33,11,4,34,12],[3,145,115,1,146,116],[4,64,40,5,65,41],[11,36,16,5,37,17],[11,36,12,5,37,13],[5,109,87,1,110,88],[5,65,41,5,66,42],[5,54,24,7,55,25],[11,36,12],[5,122,98,1,123,99],[7,73,45,3,74,46],[15,43,19,2,44,20],[3,45,15,13,46,16],[1,135,107,5,136,108],[10,74,46,1,75,47],[1,50,22,15,51,23],[2,42,14,17,43,15],[5,150,120,1,151,121],[9,69,43,4,70,44],[17,50,22,1,51,23],[2,42,14,19,43,15],[3,141,113,4,142,114],[3,70,44,11,71,45],[17,47,21,4,48,22],[9,39,13,16,40,14],[3,135,107,5,136,108],[3,67,41,13,68,42],[15,54,24,5,55,25],[15,43,15,10,44,16],[4,144,116,4,145,117],[17,68,42],[17,50,22,6,51,23],[19,46,16,6,47,17],[2,139,111,7,140,112],[17,74,46],[7,54,24,16,55,25],[34,37,13],[4,151,121,5,152,122],[4,75,47,14,76,48],[11,54,24,14,55,25],[16,45,15,14,46,16],[6,147,117,4,148,118],[6,73,45,14,74,46],[11,54,24,16,55,25],[30,46,16,2,47,17],[8,132,106,4,133,107],[8,75,47,13,76,48],[7,54,24,22,55,25],[22,45,15,13,46,16],[10,142,114,2,143,115],[19,74,46,4,75,47],[28,50,22,6,51,23],[33,46,16,4,47,17],[8,152,122,4,153,123],[22,73,45,3,74,46],[8,53,23,26,54,24],[12,45,15,28,46,16],[3,147,117,10,148,118],[3,73,45,23,74,46],[4,54,24,31,55,25],[11,45,15,31,46,16],[7,146,116,7,147,117],[21,73,45,7,74,46],[1,53,23,37,54,24],[19,45,15,26,46,16],[5,145,115,10,146,116],[19,75,47,10,76,48],[15,54,24,25,55,25],[23,45,15,25,46,16],[13,145,115,3,146,116],[2,74,46,29,75,47],[42,54,24,1,55,25],[23,45,15,28,46,16],[17,145,115],[10,74,46,23,75,47],[10,54,24,35,55,25],[19,45,15,35,46,16],[17,145,115,1,146,116],[14,74,46,21,75,47],[29,54,24,19,55,25],[11,45,15,46,46,16],[13,145,115,6,146,116],[14,74,46,23,75,47],[44,54,24,7,55,25],[59,46,16,1,47,17],[12,151,121,7,152,122],[12,75,47,26,76,48],[39,54,24,14,55,25],[22,45,15,41,46,16],[6,151,121,14,152,122],[6,75,47,34,76,48],[46,54,24,10,55,25],[2,45,15,64,46,16],[17,152,122,4,153,123],[29,74,46,14,75,47],[49,54,24,10,55,25],[24,45,15,46,46,16],[4,152,122,18,153,123],[13,74,46,32,75,47],[48,54,24,14,55,25],[42,45,15,32,46,16],[20,147,117,4,148,118],[40,75,47,7,76,48],[43,54,24,22,55,25],[10,45,15,67,46,16],[19,148,118,6,149,119],[18,75,47,31,76,48],[34,54,24,34,55,25],[20,45,15,61,46,16]],j.getRSBlocks=function(a,b){var c=j.getRsBlockTable(a,b);if(void 0==c)throw new Error("bad rs block @ typeNumber:"+a+"/errorCorrectLevel:"+b);for(var d=c.length/3,e=[],f=0;d>f;f++)for(var g=c[3*f+0],h=c[3*f+1],i=c[3*f+2],k=0;g>k;k++)e.push(new j(h,i));return e},j.getRsBlockTable=function(a,b){switch(b){case d.L:return j.RS_BLOCK_TABLE[4*(a-1)+0];case d.M:return j.RS_BLOCK_TABLE[4*(a-1)+1];case d.Q:return j.RS_BLOCK_TABLE[4*(a-1)+2];case d.H:return j.RS_BLOCK_TABLE[4*(a-1)+3];default:return void 0}},k.prototype={get:function(a){var b=Math.floor(a/8);return 1==(1&this.buffer[b]>>>7-a%8)},put:function(a,b){for(var c=0;b>c;c++)this.putBit(1==(1&a>>>b-c-1))},getLengthInBits:function(){return this.length},putBit:function(a){var b=Math.floor(this.length/8);this.buffer.length<=b&&this.buffer.push(0),a&&(this.buffer[b]|=128>>>this.length%8),this.length++}};var l=[[17,14,11,7],[32,26,20,14],[53,42,32,24],[78,62,46,34],[106,84,60,44],[134,106,74,58],[154,122,86,64],[192,152,108,84],[230,180,130,98],[271,213,151,119],[321,251,177,137],[367,287,203,155],[425,331,241,177],[458,362,258,194],[520,412,292,220],[586,450,322,250],[644,504,364,280],[718,560,394,310],[792,624,442,338],[858,666,482,382],[929,711,509,403],[1003,779,565,439],[1091,857,611,461],[1171,911,661,511],[1273,997,715,535],[1367,1059,751,593],[1465,1125,805,625],[1528,1190,868,658],[1628,1264,908,698],[1732,1370,982,742],[1840,1452,1030,790],[1952,1538,1112,842],[2068,1628,1168,898],[2188,1722,1228,958],[2303,1809,1283,983],[2431,1911,1351,1051],[2563,1989,1423,1093],[2699,2099,1499,1139],[2809,2213,1579,1219],[2953,2331,1663,1273]],o=function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){function g(a,b){var c=document.createElementNS("http://www.w3.org/2000/svg",a);for(var d in b)b.hasOwnProperty(d)&&c.setAttribute(d,b[d]);return c}var b=this._htOption,c=this._el,d=a.getModuleCount();Math.floor(b.width/d),Math.floor(b.height/d),this.clear();var h=g("svg",{viewBox:"0 0 "+String(d)+" "+String(d),width:"100%",height:"100%",fill:b.colorLight});h.setAttributeNS("http://www.w3.org/2000/xmlns/","xmlns:xlink","http://www.w3.org/1999/xlink"),c.appendChild(h),h.appendChild(g("rect",{fill:b.colorDark,width:"1",height:"1",id:"template"}));for(var i=0;d>i;i++)for(var j=0;d>j;j++)if(a.isDark(i,j)){var k=g("use",{x:String(i),y:String(j)});k.setAttributeNS("http://www.w3.org/1999/xlink","href","#template"),h.appendChild(k)}},a.prototype.clear=function(){for(;this._el.hasChildNodes();)this._el.removeChild(this._el.lastChild)},a}(),p="svg"===document.documentElement.tagName.toLowerCase(),q=p?o:m()?function(){function a(){this._elImage.src=this._elCanvas.toDataURL("image/png"),this._elImage.style.display="block",this._elCanvas.style.display="none"}function d(a,b){var c=this;if(c._fFail=b,c._fSuccess=a,null===c._bSupportDataURI){var d=document.createElement("img"),e=function(){c._bSupportDataURI=!1,c._fFail&&_fFail.call(c)},f=function(){c._bSupportDataURI=!0,c._fSuccess&&c._fSuccess.call(c)};return d.onabort=e,d.onerror=e,d.onload=f,d.src="data:image/gif;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAFCAYAAACNbyblAAAAHElEQVQI12P4//8/w38GIAXDIBKE0DHxgljNBAAO9TXL0Y4OHwAAAABJRU5ErkJggg==",void 0}c._bSupportDataURI===!0&&c._fSuccess?c._fSuccess.call(c):c._bSupportDataURI===!1&&c._fFail&&c._fFail.call(c)}if(this._android&&this._android<=2.1){var b=1/window.devicePixelRatio,c=CanvasRenderingContext2D.prototype.drawImage;CanvasRenderingContext2D.prototype.drawImage=function(a,d,e,f,g,h,i,j){if("nodeName"in a&&/img/i.test(a.nodeName))for(var l=arguments.length-1;l>=1;l--)arguments[l]=arguments[l]*b;else"undefined"==typeof j&&(arguments[1]*=b,arguments[2]*=b,arguments[3]*=b,arguments[4]*=b);c.apply(this,arguments)}}var e=function(a,b){this._bIsPainted=!1,this._android=n(),this._htOption=b,this._elCanvas=document.createElement("canvas"),this._elCanvas.width=b.width,this._elCanvas.height=b.height,a.appendChild(this._elCanvas),this._el=a,this._oContext=this._elCanvas.getContext("2d"),this._bIsPainted=!1,this._elImage=document.createElement("img"),this._elImage.style.display="none",this._el.appendChild(this._elImage),this._bSupportDataURI=null};return e.prototype.draw=function(a){var b=this._elImage,c=this._oContext,d=this._htOption,e=a.getModuleCount(),f=d.width/e,g=d.height/e,h=Math.round(f),i=Math.round(g);b.style.display="none",this.clear();for(var j=0;e>j;j++)for(var k=0;e>k;k++){var l=a.isDark(j,k),m=k*f,n=j*g;c.strokeStyle=l?d.colorDark:d.colorLight,c.lineWidth=1,c.fillStyle=l?d.colorDark:d.colorLight,c.fillRect(m,n,f,g),c.strokeRect(Math.floor(m)+.5,Math.floor(n)+.5,h,i),c.strokeRect(Math.ceil(m)-.5,Math.ceil(n)-.5,h,i)}this._bIsPainted=!0},e.prototype.makeImage=function(){this._bIsPainted&&d.call(this,a)},e.prototype.isPainted=function(){return this._bIsPainted},e.prototype.clear=function(){this._oContext.clearRect(0,0,this._elCanvas.width,this._elCanvas.height),this._bIsPainted=!1},e.prototype.round=function(a){return a?Math.floor(1e3*a)/1e3:a},e}():function(){var a=function(a,b){this._el=a,this._htOption=b};return a.prototype.draw=function(a){for(var b=this._htOption,c=this._el,d=a.getModuleCount(),e=Math.floor(b.width/d),f=Math.floor(b.height/d),g=['<table style="border:0;border-collapse:collapse;">'],h=0;d>h;h++){g.push("<tr>");for(var i=0;d>i;i++)g.push('<td style="border:0;border-collapse:collapse;padding:0;margin:0;width:'+e+"px;height:"+f+"px;background-color:"+(a.isDark(h,i)?b.colorDark:b.colorLight)+';"></td>');g.push("</tr>")}g.push("</table>"),c.innerHTML=g.join("");var j=c.childNodes[0],k=(b.width-j.offsetWidth)/2,l=(b.height-j.offsetHeight)/2;k>0&&l>0&&(j.style.margin=l+"px "+k+"px")},a.prototype.clear=function(){this._el.innerHTML=""},a}();QRCode=function(a,b){if(this._htOption={width:256,height:256,typeNumber:4,colorDark:"#000000",colorLight:"#ffffff",correctLevel:d.H},"string"==typeof b&&(b={text:b}),b)for(var c in b)this._htOption[c]=b[c];"string"==typeof a&&(a=document.getElementById(a)),this._android=n(),this._el=a,this._oQRCode=null,this._oDrawing=new q(this._el,this._htOption),this._htOption.text&&this.makeCode(this._htOption.text)},QRCode.prototype.makeCode=function(a){this._oQRCode=new b(r(a,this._htOption.correctLevel),this._htOption.correctLevel),this._oQRCode.addData(a),this._oQRCode.make(),this._el.title=a,this._oDrawing.draw(this._oQRCode),this.makeImage()},QRCode.prototype.makeImage=function(){"function"==typeof this._oDrawing.makeImage&&(!this._android||this._android>=3)&&this._oDrawing.makeImage()},QRCode.prototype.clear=function(){this._oDrawing.clear()},QRCode.CorrectLevel=d}();
function McLoadingImages($rootScope) {
    "use strict";

    var deferredLoadImages = new McDeferredLoadImages();
    var loadingImgList = {};
    var self = this;

    function getImgFromLoadList(hash) {
        return loadingImgList[hash] && loadingImgList[hash].idx && loadingImgList[hash].idx.length ? loadingImgList[hash].idx.shift() : null;
    }
    function getImgNameFromLoadList(hash) {
        return loadingImgList[hash] ? loadingImgList[hash].name : null;
    }
    function getTaskIdLoadList(hash) {
        return loadingImgList[hash] && loadingImgList[hash].ID && loadingImgList[hash].ID.length ? loadingImgList[hash].ID.shift() : null;
    }
    function runLoad(queue, idx, done, onLoadImg){
        self.loadThumbs([queue[idx], getImgNameFromLoadList(queue[idx])], onLoadImg, function () {
            idx ++;

            if (idx < queue.length){
                runLoad(queue, idx, done, onLoadImg);
            } else
            if (done) {
                done();
            }
        });
    }

    self.loadAllThumbs = function(done, onLoadImg){
        if (!deferredLoadImages){
            deferredLoadImages = new McDeferredLoadImages();
        }

        var queue = [];

        Object.keys(loadingImgList).forEach(function (hash) {
            loadingImgList[hash].idx.forEach(function () {
                queue.push(hash);
            });
        });

        runLoad(queue, 0, done, onLoadImg);
    };

    self.loadThumbs = function (info, cb, done) {
        var fileInfo = mcService.isObject(info) ? info : {
            Hash    : info[0],
            FileName: info[1],
            idx     : info[2],
            tmp     : info[3]
        };

        if (fileInfo.FileName){
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_get_image_thumbs,
                mcConst.SessionID,

                fileInfo.Hash,                //info[0] , //.Hash     , // SHA1 хэш файла
                $rootScope.thumbsSize.x     , // произвольная ширина уменьшеной копии изображения
                $rootScope.thumbsSize.y     , // произвольная высота уменьшенной копии изображения
                fileInfo.FileName,            //info[1] , // FileName , // локальное название файла
                mcService.getCurrentChantTypeForFiles($rootScope.GetChatType()),

                function (requestData) {
                    var idx   = getImgFromLoadList(requestData.Hash) || fileInfo.idx;
                    var where = "/" + $rootScope.GetChatType().toLowerCase() + "/";

                    if (idx) {
                        var _link   = document.getElementById('link_' + requestData.Hash + idx);
                        var _imgOK  = document.getElementById('img_' + requestData.Hash + idx);
                        var fileName= mcService.extractFileName(requestData.FileName);
                        var hasError= false;

                        if (where !== "/kanban/" || where !== "/forum/") {
                            where = "/";
                        }

                        var lnkHref = mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasFiles + where + requestData.Hash + "/" + (requestData.Exists ? encodeURI(fileName) : mcConst.imagesPath.nofile);

                        if (requestData.Body && requestData.Body.length) {
                            if (!(_imgOK && _link)) return;
                            
                            _imgOK.src       = "data:image/jpeg;base64," + requestData.Body;
                            _imgOK.className = "finger";
                            _link.href       = lnkHref;

                            cb && cb();
                        } else {
                            var imgSrc  = requestData.Exists && ($rootScope.isWebClient || mcConst.ClientSettings.SysEventsShowImagesInChat)
                                ? mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasFiles + where + requestData.Hash + '-' + $rootScope.thumbsSize.xy + '.jpg'
                                : mcConst.imagesPath.nofile;

                            if (_imgOK && _imgOK.src && _link && _link.href){
                                _imgOK.src = imgSrc;
                                _link.href = lnkHref;

                                _imgOK.onload = function () {
                                    if (!hasError) {
                                        _imgOK.className = "";
                                    }

                                    if (cb) {
                                        cb(this.height, requestData, getTaskIdLoadList(requestData.Hash), imgSrc, lnkHref);
                                    }
                                };

                                _imgOK.onerror = _imgOK.onabort = function (err) {
                                    console.warn(err);

                                    hasError = true;

                                    _imgOK.src = mcConst.imagesPath.noImage;
                                    _imgOK.className = "noImageClick";

                                    try {
                                        this.parentElement.style.height  = "auto";
                                        this.parentElement.style.width   = "auto";
                                        this.parentElement.style.display = "inline";
                                    } catch (e) {}
                                };
                            } else

                            if (deferredLoadImages) {
                                deferredLoadImages.addImage(idx, requestData.Hash, imgSrc, lnkHref);
                            }
                        }
                    } else
                    if (requestData.Exists){
                        var _imgTemp = document.getElementById('img_temp_' + requestData.Hash + "_" + fileInfo.tmp);

                        if (_imgTemp){
                            where = "/kanban/";

                            _imgTemp.src = requestData.Exists && ($rootScope.isWebClient || mcConst.ClientSettings.SysEventsShowImagesInChat)
                                ? mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasFiles + where + requestData.Hash + '-' + $rootScope.thumbsSize.xy + '.jpg'
                                : mcConst.imagesPath.nofile;

                            _imgTemp.onload = function () {
                                cb && cb();
                            };

                            _imgTemp.onerror = _imgTemp.onabort = function (err) {
                                console.warn(err);

                                hasError = true;

                                _imgTemp.src = mcConst.imagesPath.noImage;

                                try {
                                    this.parentElement.style.height  = "auto";
                                    this.parentElement.style.width   = "auto";
                                    this.parentElement.style.display = "inline";
                                } catch (e) {}
                            };
                        }
                    } else
                    if (!requestData.Exists){
                        var _imgNO  = mcService.getByCssClass('img_temp_' + requestData.Hash);

                        if (_imgNO && _imgNO.length) {
                            for (var i = 0; i < _imgNO.length; i++){
                                _imgNO[i].src = mcConst.imagesPath.noImage;

                                _imgNO[i].parentElement.onclick = function (e) {
                                    e.preventDefault();
                                };
                            }
                        }
                    }

                    if (done){
                        done(requestData);
                    }
                }
            ]);
        }
    };

    self.loadAllImages = function (cb) {
        if (!deferredLoadImages){
            deferredLoadImages = new McDeferredLoadImages();
        }

        deferredLoadImages.load(cb);
    };

    self.addImageToLoad = function (hash, idx, name, taskID) {
        if (!loadingImgList.hasOwnProperty(hash)) {
            loadingImgList[hash] = {
                idx : [idx],
                name: name
            };

            if (taskID) {
                loadingImgList[hash].ID = [taskID];
            }
        } else {
            if (mcService.inArray(idx, loadingImgList[hash].idx) === -1){
                loadingImgList[hash].idx.push(idx);

                if (taskID){
                    if (loadingImgList[hash].ID){
                        loadingImgList[hash].ID.push(taskID);
                    } else {
                        loadingImgList[hash].ID = [taskID];
                    }
                }
            }
        }
    };

    self.destroy = function () {
        deferredLoadImages = null;
    };

    return self;
}

// ==============   DEFERRED LOAD IMAGES  =======================

function McDeferredLoadImages() {
    var items = {};

    this.addImage = function (idx, hash, src, href) {
        if (!items.hasOwnProperty(idx)) {
            items[idx] = [hash, src, href];
        }
    };

    this.load = function (cb) {
        var count      = Object.keys(items).length;
        var currentIdx = 0;

        Object.keys(items).forEach(function (idx) {
            var hash    = items[idx][0];
            var imgSrc  = items[idx][1];
            var lnkHref = items[idx][2];
            var hasError= false;

            var _link   = document.getElementById('link_' + hash + idx);
            var _img    = document.getElementById('img_' + hash + idx);

            if (_img && _img.src && _link && _link.href){
                _img.src   = imgSrc;
                _link.href = lnkHref;

                console.log('loadAll: ' + imgSrc);

                _img.onload = function () {
                    currentIdx ++;

                    if (!hasError) {
                        _img.className = "";
                    }

                    delete items[idx];

                    if (currentIdx === count && cb){
                        cb();
                    }
                };

                _img.onerror =_img.onabort = function (err) {
                    console.warn(err);

                    hasError = true;

                    _img.src = mcConst.imagesPath.noImage;
                    _img.className = "noImageClick";

                    try {
                        this.parentElement.style.height  = "auto";
                        this.parentElement.style.width   = "auto";
                        this.parentElement.style.display = "inline";
                    } catch (e) {}
                }
            }
        });
    };
}

function McVisualisation($rootScope, convUIN) {
    "use strict";

    var _self          = this;
    var _uin           = null;
    var rnd            = mcService.RandomHash(5);
    var items          = {};
    var vFileUploader  = null;
    var vDirectReceive = null;
    var downloadConstants = {
        NOPROCESS       : 0,
        UPLOAD          : 1,
        UPLOADBUFFER    : 2,
        DOWNLOAD        : 3,
        PREPARING       : 4,
        UPLOADIRECT     : 5,
        WAITANSWER      : 6,
        DOWNLOADPREP    : 7,
        DOWNLOADDIRECT  : 8,
        DOWNLOADFTP     : 9
    };
    var currentDuState = downloadConstants.NOPROCESS;
    var rayID = "";

    function __abortTransfer(allOK) {
        switch (currentDuState){
            case downloadConstants.DOWNLOADFTP:
                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_ftp_abort
                ]);
                break;

            case downloadConstants.DOWNLOAD:
                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_file_download_abort
                ]);
                break;

            case downloadConstants.WAITANSWER:
                if (vFileUploader && vFileUploader.getUINsend()){
                    $rootScope.$broadcast('sendCMDToElectron', [
                        mcConst._CMD_.ce_file_direct_upload_abort
                    ]);
                }

                $rootScope.SendCMDToServer([
                    mcConst._CMD_.cs_files_transfer_request_abort,
                    mcConst.SessionID,
                    $rootScope.GetChatID()
                ]);
                break;

            case downloadConstants.UPLOADIRECT:
                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_file_direct_upload_abort
                ]);

                $rootScope.SendCMDToServer([
                    mcConst._CMD_.cs_files_transfer_abort,
                    mcConst.SessionID,
                    $rootScope.GetChatID()
                ]);
                break;

            case downloadConstants.UPLOADBUFFER:
            case downloadConstants.UPLOAD:
                $rootScope.$broadcast('abortUploading');
                break;

            case downloadConstants.PREPARING:
                $rootScope.$broadcast('abortPreparingUploading');
                break;

            case downloadConstants.DOWNLOADPREP:
                vDirectReceive.reject(true);
                break;

            case downloadConstants.DOWNLOADDIRECT:
                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_remove_ftp_user,
                    _uin
                ]);

                if (!allOK) {
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_files_transfer_abort,
                        mcConst.SessionID,
                        $rootScope.GetChatID()
                    ]);
                }
                break;
        }

        _self._hideProgressInfo();
    }

    function _acceptFilesReceive() {

    }

    // ===========================================================

    this._getViews  = function (cancelText) {
        return {
            progressWrapper:
                { id: "progressWrapper" + rnd, css: "noBGColor byCenter borderTopBottom", height: 30, padding: 0, borderless: true, hidden: true, cols: [
                    { id: "universalProgress" + rnd, borderless: true, css: "noBGColor byCenter", hidden: true,
                        template: function () {
                            var html = "&nbsp;";

                            if (this.progress){
                                var data = this.progress;

                                html = mcService.myReplaceFormated(
                                    "<div class='uploader_overall'>" +
                                    "   <div class='uploader_status'>" +
                                    "       <div class='uploader_progress' style='width:#{width}'></div>" +
                                    "       <div class='uploader_message'>#{txt}</div>" +
                                    "   </div>"+
                                    "</div>",
                                    {
                                        width   : data.percent !== undefined ? data.percent + "%" : 0,
                                        txt     : data.text + (data.size ? "/" + data.size + " " : " ") + (data.percent !== undefined ? data.percent + "%" : "")
                                    }
                                );
                            }

                            return html;
                        }
                    },

                    { id: "uploadProgress" + rnd, view: "list", type: "myUploader", css: "uploader_nopadding", scroll: false, padding: 0, borderless: true, hidden: true, select: false},
                    { view: "button", value: cancelText || mcLang(33), width: 105, click: function () { // "33" :"Отмена",
                        __abortTransfer();

                        _self.onAbortButton && _self.onAbortButton();
                    }}
                ]},

            downloadFilesQuestion:
                { id: "downloadFilesQuestion" + rnd, css: "borderTopBottom", borderless: true, height: 60, hidden: true, rows: [
                    { id: "downloadFileList", view: "template", template: "&nbsp;", padding: 5, borderless: true, css: "whiteBg monospaceAll", scroll: 'y'},

                    { cols: [
                        {},
                        { view: "button", value: mcLang(592), maxWidth: 130, click: _acceptFilesReceive}, // "592":"Принять",
                        { width: 10},
                        { view: "button", value: mcLang(33),  maxWidth: 130, click: __abortTransfer, type: "danger"}, // "33" :"Отмена",
                        {}
                    ]}
                ]}
        }
    };

    this.getUploadProgress = function () {
        return "uploadProgress" + rnd;
    };

    this._initViews = function () {
        items.progressWrapper       = $$("progressWrapper" + rnd);
        items.universalProgress     = $$("universalProgress" + rnd);
        items.uploadProgress        = $$("uploadProgress" + rnd);
    };

    this._showDownloadFileList = function (list) {
        _uin = list.UIN;

        $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
            mcConst._CMD_.ce_get_user_folder,

            list.UIN,
            list.DisplayName,

            function (path) {
                vDirectReceive.setFilesList(list, path);

                if ($rootScope.customUserOptions.getOption(_uin, "autoReceive")) {
                    setTimeout(function () {
                        vDirectReceive.accept(path);
                    }, 100);
                } else {
                    // $rootScope.$broadcast("unPinTool");
                    $rootScope.$broadcast("selectTool", [mcConst.dataModels.ReceiveFiles]);
                    $rootScope.$broadcast(window._messages_.receiveFiles.setReceiveFilesInfo, [vDirectReceive.getInfo()]);
                }
            }
        ]);
    };

    this._hideProgressInfo = function (allOk){
        if (currentDuState && !allOk && window._messages_.chatFrame){
            // if (!allOk && window._messages_.chatFrame) {
            $rootScope.$broadcast(window._messages_.chatFrame.scrollDownChat);
            // }
        }

        if (allOk){
            switch (currentDuState){
                case downloadConstants.DOWNLOAD:
                case downloadConstants.DOWNLOADFTP:
                    webix.message(mcLang(575)); // "575":"Файл скачан",
                break;

                case  downloadConstants.UPLOAD:
                    webix.message(mcLang(576)); // "576":"Файл загружен на сервер",
                break;
            }
        }

        // currentDuStat e = downloadConstants.NOPROCESS;

        if (items.progressWrapper) {
            _self._showProgressInfo(downloadConstants.NOPROCESS, true);
        }
    };

    this._showProgressInfo = function (state, clear){
        currentDuState = state;

        if (currentDuState !== downloadConstants.NOPROCESS && currentDuState !== downloadConstants.DOWNLOADPREP) {
            items.progressWrapper.show();
        } else {
            items.progressWrapper.hide();
        }

        if (clear){
            items.uploadProgress.clearAll();
        }

        switch (currentDuState){
            case downloadConstants.PREPARING:
            case downloadConstants.DOWNLOAD:
            case downloadConstants.DOWNLOADFTP:
            case downloadConstants.UPLOADBUFFER:
            case downloadConstants.UPLOADIRECT:
            case downloadConstants.DOWNLOADDIRECT:
                _self._updateProgress({
                    percent : 0,
                    text    : ' ... '
                });

                items.universalProgress.show();
                items.uploadProgress.hide();
            break;

            case downloadConstants.WAITANSWER:
                _self._updateProgress({
                    text: mcLang(583) // "583":"Ожидаем согласие на прием файлов...",
                });

                items.universalProgress.show();
                items.uploadProgress.hide();
            break;

            case downloadConstants.UPLOAD:
                items.universalProgress.hide();
                items.uploadProgress.show();
            break;
        }
    };

    this._updateProgress = function (chunk) {
        if (currentDuState){
            items.universalProgress.define('progress', chunk);
            items.universalProgress.refresh();
        }
    };

    this._onPrepareProgress = function (percent) {
        _self._updateProgress({
            percent: percent,
            text   : 'SHA1 calculating'
        });
    };

    this.abortTransfer = __abortTransfer;

    this.setFileUploader = function (_fileUploader) {
        vFileUploader = _fileUploader;
    };

    this.setDirectReceive = function (_directReceive) {
        vDirectReceive = _directReceive;
    };

    this.onAbortButton = null;

    this.active = function (_rayID) {
        // if (!rayID || _rayID === rayID) {
        //     rayID = _rayID;
        //     return true;
        // } else {
        //     return false;
        // }
        return !!items.progressWrapper;
    };

    this.getState = function () {
        return currentDuState;
    };

    this.constants = downloadConstants;
}
function McDirectReceive($rootScope) {
    "use strict";

    var fromUIN    = -1;
    var _list      = {};
    var _totalSize = 0;
    var _totalCnt  = 0;
    var TERMINATOR = mcConst.terminator;
    var self       = this;
    var DisplayName= "";
    var downloadPath = "";
    var drVisualisation = null;
    var DOWNLOADDIRECT = 8;

    function parseList(list) {
        var files = list.split(TERMINATOR); // (idx + 1) + TERMINATOR + fileName + TERMINATOR + path + TERMINATOR + size + TERMINATOR + attr + TERMINATOR
        var cnt   = 0;

        _list = {};

        while (cnt++ < _totalCnt){
            var idx      = files.shift();
            var fileName = files.shift();
            var path     = files.shift();
            var size     = files.shift();
            var attr     = files.shift();

            _list[idx] = {
                fileName: fileName,
                path    : path,
                size    : size,
                attr    : attr
            };
        }
    }

    function getList() {
        return mcService.convertObjToArray(_list, "idx", true);
    }

    function clear() {
        fromUIN    = -1;
        _list      = {};
        _totalCnt  = 0;
        _totalSize = 0;
        downloadPath = "";
    }

    self.clear = clear;

    self.setFilesList = function (filesInfo, _path) {
        if (!mcService.isObjectEmpty(_list)){
            clear();
        }

        _totalSize = filesInfo.FilesSize;
        _totalCnt  = filesInfo.FilesCount;
        DisplayName= filesInfo.DisplayName;
        fromUIN    = filesInfo.UIN;
        downloadPath = _path;

        parseList(filesInfo.List);
    };

    self.getCurrentReceiveUIN = function () {
        return fromUIN;
    };

    self.getInfo = function () {
        return {
            FilesList   : getList(),
            DisplayName : DisplayName,
            UIN         : fromUIN,
            Size        : _totalSize,
            Count       : _totalCnt,
            DownloadPath: downloadPath
        };
    };

    self.reject = function (prepare) {
        $rootScope.$broadcast(window._messages_.chatWrapper.hideTool, [mcConst.dataModels.ReceiveFiles]);

        if (!prepare){
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_files_transfer_deny,
                mcConst.SessionID,

                fromUIN
            ]);
        }

        clear();
    };

    self.accept = function (path) {
        if (!$rootScope.customUserOptions.getOption(fromUIN, "autoReceive")) {
            $rootScope.$broadcast(window._messages_.chatWrapper.hideTool, [mcConst.dataModels.ReceiveFiles]);
            $rootScope.$broadcast("selectDialog", ["UIN-" + fromUIN]);
        }

        $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
            mcConst._CMD_.ce_file_direct_receive_start,
            fromUIN,
            path,

            function (info) {
                $rootScope.SendCMDToServer([
                    mcConst._CMD_.cs_files_transfer_accept,
                    mcConst.SessionID,

                    fromUIN        ,  // идентификатор отправителя файлов
                    info.port      ,  // TCP порт получателя, на который будут передаваться файлы
                    info.interfaces,  // список локальных сетевых интерфейсов получателя
                    ""             ,  // индексы файлов, которые получатель не хочет принимать. Может быть пустым. Индексы перечисляются в текстовой строке через запятую
                    512               // размер буфера для передачи файлов через сервер, если передача напрямую не удастся по техническим причинам
                ]);

                drVisualisation._showProgressInfo(DOWNLOADDIRECT);
            }
        ]);

        $rootScope.$broadcast(window._messages_.dialogsList.OpenPrivate, [fromUIN]);

        self.onAcceptReceive && self.onAcceptReceive(fromUIN);
    };

    self.addReceiveFile = function (info) {
        if (info.UIN === fromUIN){
            _list[info.FileIdx].received = true;
        }
    };

    self.setVisualisation = function (_visualisation) {
        drVisualisation = _visualisation;
    };

    self.onAcceptReceive = null;
}
function McUploaderViaServer($rootScope, uploadOptions) {
    "use strict";

    var _uin           = null;
    var _filesInfo     = null;
    var fVisualisation = null;
    var Self           = this;
    var UPLOAD         = 1;
    var PREPARING      = 4;
    var pastView       = null;

    function __sendFileMessage(info, allok) {
        // uploadOptions.done(
        //     info.chatType,
        //     info.ID,
        //     info.Hash + mcConst.CRLF + info.FileName + mcConst.CRLF + mcService.fileTimeStamp(info.lastModifiedDate) +
        //                 mcConst.CRLF + info.fileSize + mcConst.CRLF + (info.resolution ? info.resolution : "-") +
        //                 (info.cText ? (mcConst.CRLF + info.cText) : ""), // excel = cText is not empty
        //     info.Type
        // );

        uploadOptions.done(
            info.chatType,
            info.ID,
            JSON.stringify({
                Hash        : info.Hash,
                FileName    : info.FileName,
                UTCWriteTime: mcService.fileTimeStamp(info.lastModifiedDate),
                Size        : info.fileSize,
                Resolution  : info.resolution ? info.resolution : "-",
                Desc        : info.cText ? info.cText : ""
            }),
            info.Type
        );

        fVisualisation._hideProgressInfo(allok);
    }
    function __setUIN(uin, _type) {
        if (_type === $rootScope.chatAliases.UIN) {
            _uin = uin;
        }
    }
    function __getUIN() {
        return _uin;
    }
    function __clearUIN() {
        _uin = null;
        _filesInfo = null;

        fVisualisation.abortTransfer();
    }
    function __onBeforeFileAdd() {
        if (uploadOptions.onBeforeFileAdd && uploadOptions.onBeforeFileAdd.apply(null, arguments)){
            __clearUIN();
        } else {
            fVisualisation._showProgressInfo(PREPARING, true);
        }
    }
    function __getFilesInfo() {
        return _filesInfo;
    }
    function __onAfterFileAdd() {
        if (uploadOptions.onAfterFileAdd && uploadOptions.onAfterFileAdd.apply(null, arguments)){
            __clearUIN();
        } else {
            fVisualisation._showProgressInfo(UPLOAD);
        }
    }
    function __onBeforeUpload(hash) { // todo: upload list

    }
    function __doUploadImage(_ID, type, image, resolution, cText) {
        __setUIN(_ID, type);

        $rootScope.$broadcast(window._messages_.mcFileUploader.uploadFile, [
            function error() { // info
                fVisualisation._hideProgressInfo();

                __clearUIN();

                webix.alert('Error file uploading!');
            },
            function success() { // file
                var info = this;

                __clearUIN();

                // info.Type       = cText === void(0) ? mcConst._CMD_.msgType.IMAGE_NEW : mcConst._CMD_.msgType.CLIPBOARD_EXCEL;
                info.Type       = mcConst._CMD_.msgType.IMAGE_NEW; // todo: добавить обработку CLIPBOARD_EXCEL для нового типа сообщений
                info.chatType   = type;
                info.ID         = _ID;
                info.fileSize   = image.size;
                info.resolution = resolution;
                info.cText      = cText;

                if (info.FileName === "image.png"){
                    info.FileName = "image_" + (new Date()).getTime() + ".png";
                }

                __sendFileMessage(info);
            }, {
                onBeforeFileAdd  : __onBeforeFileAdd,
                onPrepareProgress: fVisualisation._onPrepareProgress,
                onAfterFileAdd   : __onAfterFileAdd,
                uploadProgress   : fVisualisation.getUploadProgress(),

                clipboardImage   : image, // Получаем файл как Blob (бинарные данные)
                Where            : mcService.getCurrentChantTypeForFiles($rootScope.GetChatType()),
                ID               : _ID
            }
        ]);
    }
    function __onPastFile(e) {
        if (!fVisualisation.getState() && (
            ((mcService.checkRights(mcConst._CMD_.RS.QTxtConfSendFiles) || mcService.checkRights(mcConst._CMD_.RS.QSendImages))     && $rootScope.GetChatType() === $rootScope.chatAliases.UID) ||
            ((mcService.checkRights(mcConst._CMD_.RS.QPrivInsertFiles)  || mcService.checkRights(mcConst._CMD_.RS.QImagesPrivates)) && $rootScope.GetChatType() === $rootScope.chatAliases.UIN)
        )) {
            var type    = $rootScope.GetChatType();
            var cText   = "";
            var imgInfo = [];

            // if (e.clipboardData.items.length) {
            //     for (var i = e.clipboardData.items.length - 1; i >= 0; i--) {
            //         if (e.clipboardData.items[i].kind === "file" && e.clipboardData.items[i].type === "image/png") {
            //             e.preventDefault();
            //
            //             Promise.all(Array.prototype.map.call(e.clipboardData.items, function (item){
            //                 if (item.kind === "string" && item.type === "text/plain"){
            //                     return item.getAsString(function (txt){
            //                         cText = txt
            //                     });
            //                 } else
            //                 if (item.kind === "file" && (item.type === "image/png" || item.type === "image/jpg" || item.type === "image/jpeg") ){
            //                     return new Promise(function (resolve){
            //                         var image   = item.getAsFile();
            //                         var _ID     = $rootScope.GetChatID();
            //                         var tempImg = document.createElement("img");
            //
            //                         tempImg.src = window.URL.createObjectURL(image);
            //
            //                         tempImg.onload = function () {
            //                             var resolution = tempImg.width + "x" + tempImg.height;
            //                             tempImg.src = "";
            //                             tempImg     = null;
            //
            //                             if (Self.pastImagePreview) {
            //                                 Self.pastImagePreview(image, function () { // comment
            //                                     imgInfo = [_ID, type, image, resolution];
            //
            //                                     resolve();
            //                                 });
            //                             } else {
            //                                 imgInfo = [_ID, type, image, resolution];
            //
            //                                 resolve();
            //                             }
            //                         };
            //                     })
            //                 }
            //             }))
            //             .then(function (){
            //                 __doUploadImage.apply(null, imgInfo.concat(cText ? [cText] : []));
            //             })
            //             .catch(function (err){
            //                 console.error(err.messsage);
            //             });
            //
            //             break;
            //         } else
            //         if (e.clipboardData.items[i].kind === "file" && e.clipboardData.files[i].size) {
            //
            //         }
            //     }
            // } else

            if (e.clipboardData.files.length) {
                e.preventDefault();

                // если в буфере 1 файл, но несколько итемов - то это скрин из экселя со скопированным тестом таблички
                if (e.clipboardData.files.length === 1 && 
                    (e.clipboardData.files[0].type === "image/png" || e.clipboardData.files[0].type === "image/jpg" || e.clipboardData.files[0].type === "image/jpeg")) {

                    Promise.all(Array.prototype.map.call(e.clipboardData.items, function (item){
                        if (item.kind === "string" && item.type === "text/plain"){
                            return item.getAsString(function (txt){
                                cText = txt
                            });
                        } else
                        if (item.kind === "file" && (item.type === "image/png" || item.type === "image/jpg" || item.type === "image/jpeg") ){
                            return new Promise(function (resolve){
                                var image   = item.getAsFile();
                                var _ID     = $rootScope.GetChatID();
                                var tempImg = document.createElement("img");

                                tempImg.src = window.URL.createObjectURL(image);

                                tempImg.onload = function () {
                                    var resolution = tempImg.width + "x" + tempImg.height;
                                    tempImg.src = "";
                                    tempImg     = null;

                                    if (Self.pastImagePreview) {
                                        Self.pastImagePreview(image, function () { // comment
                                            imgInfo = [_ID, type, image, resolution];

                                            resolve();
                                        });
                                    } else {
                                        imgInfo = [_ID, type, image, resolution];

                                        resolve();
                                    }
                                };
                            })
                        }
                    }))
                    .then(function (){
                        __doUploadImage.apply(null, imgInfo.concat(cText ? [cText] : []));
                    })
                    .catch(function (err){
                        console.error(err.messsage);
                    });
                } else
                if (e.clipboardData.files.length >= 1) {
                    Self._uploadFile(mcConst._CMD_.msgType.FILE_NEW, true, e.clipboardData);
                }
            }/* else

            if (e.clipboardData.files.length >= 0) {
                if (fVisualisation.getState()) {
                    webix.message(mcLang(601)); // "601":"Подождите окончания приема/передачи файлов",
                } else

                if (!$rootScope.isWebClient) {
                    if (Self.pastFilePreview){
                        Self.pastFilePreview(e.clipboardData.files, function () {
                            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                                mcConst._CMD_.ce_get_clipboard_files_list
                            ]);
                        })
                    } else {
                        $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                            mcConst._CMD_.ce_get_clipboard_files_list
                        ]);
                    }
                }
            }*/
        }
    }

    this.getFilesInfo     = __getFilesInfo;
    this.getUINsend       = __getUIN;
    this.setUINsend       = __setUIN;
    this.abortUploading   = __clearUIN;
    this.sendFileMessage  = __sendFileMessage;
    this.pastImagePreview = null;
    this.pastFilePreview  = null;

    this.removePasteView  = function () {
        if (pastView) {
            pastView.removeEventListener("paste", __onPastFile);
        }
    };

    this._onPasteItemFromClipboard = function (_view){
        pastView = _view;

        pastView.addEventListener("paste", __onPastFile);
    };

    this.setVisualisation = function (_visualisation) {
        fVisualisation = _visualisation;
    };

    this._uploadFile      = function (fileType, filePath, dropFileInfo){
        function _DD_upload(_fileType, _filePath, _dropFileInfo, _currentID, _chatType) {
            __setUIN(_currentID, _chatType);

            $rootScope.$broadcast(window._messages_.mcFileUploader.uploadFile, [
                function error(){
                    fVisualisation._hideProgressInfo();

                    __clearUIN();

                    webix.alert('Error file uploading!');
                },
                function success(){
                    var info = this;

                    if (info.fileIdx === info.dropFile.files.length){
                        __clearUIN();
                    }

                    switch (_fileType){
                        case mcConst._CMD_.msgType.IMAGE_NEW:
                        // case mcConst._CMD_.msgType.IMAGE:
                            __sendFileMessage(info);
                        break;

                        default:
                            __sendFileMessage(info, true);
                    }
                }, {
                    onBeforeFileAdd     : __onBeforeFileAdd,
                    onAfterFileAdd      : __onAfterFileAdd,
                    onBeforeUpload      : __onBeforeUpload,
                    onPrepareProgress   : fVisualisation._onPrepareProgress,
                    uploadProgress      : fVisualisation.getUploadProgress(),

                    filePath: _filePath,
                    chatType: _chatType,
                    dropFile: _dropFileInfo,

                    Type  : _fileType,
                    Where : mcService.getCurrentChantTypeForFiles($rootScope.GetChatType()),
                    ID    : _currentID
                }
            ]);

            if (!$rootScope.isWebClient) {
                $rootScope.$broadcast(window._messages_.mcStatistics.setStatValue, [_dropFileInfo ? "n_file_drag" : "n_file", 1, false]); // false - увеличить значение на "1"
            }
        }

        var currentID = $rootScope.GetChatID();
        var chatType  = $rootScope.GetChatType();

        if (dropFileInfo && (mcConst.ClientSettings && mcConst.ClientSettings.PreviewDropFile)) {
            if (dropFileInfo.items.length === 1 && dropFileInfo.items[0].kind === "file" && Self.pastImagePreview &&
                (
                    dropFileInfo.items[0].type === "image/png" ||
                    dropFileInfo.items[0].type === "image/jpg" ||
                    dropFileInfo.items[0].type === "image/jpeg"||
                    dropFileInfo.items[0].type === "image/gif"
                ))
            {
                var image      = dropFileInfo.items[0].getAsFile();
                var resolution = "";
                var tempImg    = document.createElement("img");

                tempImg.src    = window.URL.createObjectURL(image);
                tempImg.onload = function () {
                    resolution   = tempImg.width + "x" + tempImg.height;
                    tempImg.src  = "";
                    tempImg      = null;

                    if (Self.pastImagePreview){
                        Self.pastImagePreview(image, function () {
                            __doUploadImage(currentID, chatType, image, resolution);
                        });
                    } else {
                        __doUploadImage(currentID, chatType, image, resolution);
                    }
                }
            } else
            if (Self.pastFilePreview) {
                var list = [];

                for (var i=0; i < dropFileInfo.files.length; i++){
                    list.push(dropFileInfo.files[i].name);
                }

                var dd = { files: mcService.Marge({ length : dropFileInfo.files.length }, dropFileInfo.files) };

                Self.pastFilePreview(list, function () {
                    _DD_upload(fileType, filePath, dd, currentID, chatType);
                });
            } else {
                _DD_upload(fileType, filePath, dropFileInfo, currentID, chatType);
            }
        } else {
            _DD_upload(fileType, filePath, dropFileInfo, currentID, chatType);
        }
    };
}
function DownloadUploadManager($rootScope, __opt) {
    "use strict";

    var options = mcService.Marge({
        name    : "",
        upload  : {
            enable          : false,
            done            : function () {},
            onBeforeFileAdd : function () {return false},
            onAfterFileAdd  : function () {return false}
        },
        download: {
            enable: false,
            ftp_download: false
        },
        direct  : {
            enable: false
        },
        images  : {
            enable: false
        }

    }, __opt);

    this._name = __opt.name;

    var duManager     = this;
    var direct        = null;
    var images        = null;
    var fileUploader  = null;
    var visualisation = null;
    var canStart      = false;
    var isStarted     = false;
    var uinDirect     = 0;

    // --- Draw Progress Bar =================================================

    visualisation = new McVisualisation($rootScope);

    // --- Functions =========================================================

    function _downloadUrl(url, fileInfo, auto) {
        if (!isStarted && !$rootScope.isWebClient && visualisation.getState() === visualisation.constants.NOPROCESS) {
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_file_download_url,
                url,
                fileInfo,
                auto
            ]);
        }
    }

    function _openDownloadUrl(url, fileInfo) {
        if (!isStarted && !$rootScope.isWebClient && visualisation.getState() === visualisation.constants.NOPROCESS) {
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_file_open_or_download,
                url,
                fileInfo
            ]);
        }
    }

    function _openInFolder(hash, uin, path) {
        $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
            mcConst._CMD_.ce_file_open_in_folder,
            hash,
            uin,
            path
        ]);
    }

    function _openFolder(path) {
        $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
            mcConst._CMD_.ce_file_open_folder,
            path
        ]);
    }

    function _sendFilesToUser(fromBuffer) {
        var uin = $rootScope.GetChatID();

        if (!isStarted && $rootScope.GetChatType() === $rootScope.chatAliases.UIN && uin && visualisation.getState() === visualisation.constants.NOPROCESS){
            $rootScope.$broadcast(window._messages_.dialogsList.getUserState, [$rootScope.GetChatID(), function (state) {
                if (mcConst.states.offline === state){
                    webix.alert(mcLang(585)); // "585":"Передавать файлы напрямую можно только пользователям онлайн!",
                } else {
                    uinDirect = uin;

                    if (!$rootScope.isWebClient && !isStarted) {
                        $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                            mcConst._CMD_.ce_file_direct_upload_prepare,
                            uinDirect,
                            fromBuffer
                        ]);
                    }
                }
            }]);
        }
    }

    function _ftpDownload(path, fileName, size) {
        if (!isStarted && !$rootScope.isWebClient) {
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_ftp_download,
                path,
                fileName, 
                size
            ]);
        }
    }

    function _destroy(){
        if (images) images.destroy();

        direct        = null;
        images        = null;
        visualisation = null;
        fileUploader  = null;
        isStarted     = false;

        uinDirect = 0;

        for (var i in window._messages_.downloadUpload){
            if (i !== '---') $rootScope.$off(window._messages_.downloadUpload[i]);
        }
    }

    // --- Direct Receive ====================================================

    if (options.direct.enable) {
        direct = new McDirectReceive($rootScope);

        direct.setVisualisation(visualisation);
        direct.onAcceptReceive = function(uin){
            uinDirect = uin;
            isStarted = true;
        };

        visualisation.setDirectReceive(direct);

        duManager.sendFilesToUser         = _sendFilesToUser;
    }

    // --- Loading Images ====================================================

    if (options.images.enable){
        images = new McLoadingImages($rootScope);

        duManager.addImageToLoad          = images.addImageToLoad;
        duManager.loadThumbs              = images.loadThumbs;
        duManager.loadAllImages           = images.loadAllImages;
        duManager.loadAllThumbs           = images.loadAllThumbs;
    }

    // --- Upload Via Server And Direct ======================================

    if (options.upload.enable){
        fileUploader = new McUploaderViaServer($rootScope, options.upload);

        fileUploader .setVisualisation(visualisation);
        visualisation.setFileUploader (fileUploader);
        visualisation.onAbortButton = function(){
            setTimeout(function () { // todo: передалать весь модуль нах
                isStarted = false;
            }, 50);
        };

        duManager.onPasteItemFromClipboard= function () {
            // if (visualisation.getState() === visualisation.constants.NOPROCESS){
                fileUploader._onPasteItemFromClipboard.apply(null, arguments);
            // }
        };
        // duManager.removePasteView         = fileUploader.
        duManager.setPastePreviewWindow   = function (cb) {
            fileUploader.pastImagePreview = cb || null;
        };
        duManager.setPastePreviewWindowFile= function (cb) {
            fileUploader.pastFilePreview = cb || null;
        };
        duManager.uploadFile              = function() {
            if (!isStarted && visualisation.getState() === visualisation.constants.NOPROCESS) {
                fileUploader._uploadFile.apply(null, arguments);
            }
        };
        duManager.abortUploading          = fileUploader.abortUploading;
        duManager.removePasteView         = fileUploader.removePasteView;
    }

    // =======================================================================

    if (options.download.enable){
        duManager.downloadUrl             = _downloadUrl;
        duManager.openDownloadUrl         = _openDownloadUrl;
        duManager.openInFolder            = _openInFolder;
        duManager.openFolder              = _openFolder;
    }

    // =======================================================================

    duManager.getViews    = visualisation._getViews;
    duManager.initViews   = visualisation._initViews;
    duManager.isActive    = visualisation.active;
    duManager.ftpDownload = _ftpDownload;
    duManager.destroy     = _destroy;
    duManager.canStart    = function () { canStart = true;  };
    duManager.noStart     = function () { canStart = false; };

    // ========================================================================
    // ========================================================================

    var _msg = window._messages_.downloadUpload = {
        changeStateForUser      : 'changeStateForUser',
        pastFilesFromClipboard  : 'pastFilesFromClipboard',
        acceptReceiveFiles      : 'acceptReceiveFiles',
        rejectReceiveFiles      : 'rejectReceiveFiles',
        sendFilesToUser         : 'sendFilesToUser',

        download_ftp_file       : 'download_ftp_file',

        on_file_download_progress      : 'on_file_download_progress',
        on_file_download_start         : 'on_file_download_start',
        on_file_download_complete      : 'on_file_download_complete',
        on_file_download_aborted       : 'on_file_download_aborted',
        on_file_download_file_not_found: 'on_file_download_file_not_found',

        on_file_upload_prepare_progress: 'on_file_upload_prepare_progress',
        on_file_upload_start           : 'on_file_upload_start',
        on_file_upload_prepare_start   : 'on_file_upload_prepare_start',
        on_file_upload_aborted         : 'on_file_upload_aborted',
        on_file_upload_progress        : 'on_file_upload_progress',
        on_file_upload_complete        : 'on_file_upload_complete',
        on_file_upload_error           : 'on_file_upload_error',

        on_file_check_exist            : 'on_file_check_exist',
        on_file_drop                   : 'on_file_drop',
        on_files_transfer_progress     : 'on_files_transfer_progress',

        on_file_request_transfer       : 'on_file_request_transfer',

        on_file_direct_upload_wait_accept         : 'on_file_direct_upload_wait_accept',
        on_file_direct_upload_accept              : 'on_file_direct_upload_accept',
        on_file_direct_upload_complete            : 'on_file_direct_upload_complete',
        on_file_direct_upload_no_files            : 'on_file_direct_upload_no_files',
        on_file_direct_download_complete          : 'on_file_direct_download_complete',
        on_file_direct_download_file_received     : 'on_file_direct_download_file_received',
        on_file_direct_receive_client_disconnected: 'on_file_direct_receive_client_disconnected',

        duManagerNoStart : 'duManagerNoStart',


        // openDownloadFolder  : 'openDownloadFolder',

        // on_file_direct_upload_file_sended   : 'on_file_direct_upload_file_sended',

        '---':'---'
    };

    // ========================================================================

    $rootScope.$on(_msg.duManagerNoStart, function () {
        duManager.noStart();
    });

    $rootScope.$on(_msg.on_files_transfer_progress, function (e, args) {
        if (isStarted && visualisation.active()) {
            visualisation._updateProgress.apply(visualisation, args);
        }
    });

    // --- download ===========================================================

    if (options.download.enable){
        if (options.download.ftp_download) {
            $rootScope.$on(_msg.download_ftp_file, function (e, args) {
                if (!isStarted && visualisation.getState() === visualisation.constants.NOPROCESS) {
                    _ftpDownload.apply(null, args);
                }
            });
        }

        $rootScope.$on(_msg.on_file_download_start, function (e, args) {
            if (!isStarted && visualisation.active.apply(null, args) && visualisation.getState() === visualisation.constants.NOPROCESS) {
                visualisation._showProgressInfo(args[1] === "ftp" ? visualisation.constants.DOWNLOADFTP : visualisation.constants.DOWNLOAD, false);
                var _savePath = args[2];

                if (_savePath) {
                    $rootScope.Storage.options.save({
                        currentDownloadFolder : _savePath
                    });

                    mcConst.currentDownloadFolder = _savePath;
                }

                // console.log(options.name);

                isStarted = true;
            }
        });

        $rootScope.$on(_msg.on_file_download_progress, function (e, args) {
            var rayID = args[0].rayID;

            if (isStarted && visualisation.active(rayID)) {
                visualisation._updateProgress.apply(visualisation, args);
            }
        });

        $rootScope.$on(_msg.on_file_download_aborted, function (e, args) {
            if (isStarted && visualisation.active.apply(null, args)) {
                visualisation._hideProgressInfo();

                setTimeout(function () { // todo: передалать весь модуль нах
                    isStarted = false;
                }, 50);
            }
        });

        $rootScope.$on(_msg.on_file_download_file_not_found, function (e, args) {
            if (isStarted && visualisation.active.apply(null, args)) {
                visualisation._hideProgressInfo();

                webix.alert("File not found on the server!");
                
                setTimeout(function () { // todo: передалать весь модуль нах
                    isStarted = false;
                }, 50);
            }
        });

        $rootScope.$on(_msg.on_file_download_complete, function (e, args) {
            if (isStarted && visualisation.active.apply(null, args) && visualisation.getState() !== visualisation.constants.NOPROCESS) {
                isStarted = false;

                visualisation._hideProgressInfo(true);

                // setTimeout(function () { // todo: передалать весь модуль нах
                // }, 50);
            }
        });
    }

    // --- upload =============================================================

    if (fileUploader || direct){
        $rootScope.$on(_msg.pastFilesFromClipboard, function (e, args) {
            if (canStart && !isStarted && visualisation.active()) {
                var files = args[0];

                if (files && files.length) { //  && $rootScope.GetChatType() === $rootScope.chatAliases.UIN
                    if (fileUploader.pastFilePreview){
                        fileUploader.pastFilePreview(files, function () {
                            fileUploader._uploadFile(mcConst._CMD_.msgType.FILE_NEW, files, false);

                            if (!$rootScope.isWebClient) $rootScope.$broadcast(window._messages_.mcStatistics.setStatValue, ["n_file_ctrlc", 1, false]); // false - увеличить значение на "1"
                        });
                    } else {
                        fileUploader._uploadFile(mcConst._CMD_.msgType.FILE_NEW, files, false);

                        if (!$rootScope.isWebClient) $rootScope.$broadcast(window._messages_.mcStatistics.setStatValue, ["n_file_ctrlc", 1, false]); // false - увеличить значение на "1"
                    }
                }
            }
        });

        $rootScope.$on(_msg.on_file_check_exist, function (e, args) {
            if (canStart && visualisation.active()) {
                // var id   = args[0].id;
                var info = args[0].info;

                if (info) {
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_is_file_exists,
                        mcConst.SessionID,

                        info.Hash,     // SHA1 хэш файла
                        info.Type,     // тип файла. 2 - изображение, 4 - обычный файл
                        info.FileName, //
                        info.Where,    // куда вставлять файл (priv, conf, bbs, broadcast, forum, kanban)
                        info.ID,       // число-идентификатор, для кого отправлять файл:
                        mcService.fileTimeStamp(info.lastModifiedDate),
                        info.fileSize,

                        function (data) {
                            if (data.Present) {
                                fileUploader.sendFileMessage(info);

                                isStarted = false;

                                $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                                    mcConst._CMD_.ce_file_check_exist,
                                    true,
                                    {
                                        Where    : info.Where,
                                        ID       : info.ID,
                                        Type     : info.Type,
                                        chatType : info.chatType
                                    }
                                ]);
                            } else {
                                $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                                    mcConst._CMD_.ce_file_check_exist,
                                    false,
                                    {
                                        Where    : info.Where,
                                        ID       : info.ID,
                                        Type     : info.Type,
                                        chatType : info.chatType
                                    }
                                ]);
                            }
                        }
                    ]);
                }
            }
        });

        $rootScope.$on(_msg.on_file_drop, function (e, args) {
            var haveRights =
                (
                    (mcService.checkRights(mcConst._CMD_.RS.QTxtConfSendFiles) || mcService.checkRights(mcConst._CMD_.RS.QSendImages)) &&
                    $rootScope.GetChatType() === $rootScope.chatAliases.UID
                ) ||
                (
                    (mcService.checkRights(mcConst._CMD_.RS.QPrivInsertFiles)  || mcService.checkRights(mcConst._CMD_.RS.QImagesPrivates)) &&
                    $rootScope.GetChatType() === $rootScope.chatAliases.UIN
                );

            if (visualisation.active() && haveRights) {
                var dataTransfer = args[0];

                !isStarted && canStart && fileUploader._uploadFile(mcConst._CMD_.msgType.FILE_NEW, true, dataTransfer);
            } else
            if (!haveRights) {
                if ($rootScope.GetChatType() === $rootScope.chatAliases.UID) {
                    $rootScope.$broadcast('ErrorMsg', [222, mcService.Error(222)]); //"222": "у вас недостаточно прав для вставки файлов в конференции",
                } else {
                    $rootScope.$broadcast('ErrorMsg', [220, mcService.Error(220)]); //"220": "у вас недостаточно прав для вставки файлов в приват",
                }
            }
        });

        $rootScope.$on(_msg.on_file_upload_start, function () {
            if (isStarted && canStart && visualisation.active()) {
                visualisation._showProgressInfo(visualisation.constants.UPLOADBUFFER);
            }
        });

        $rootScope.$on(_msg.on_file_upload_error, function (e, args) {
            var err = args[0];

            console.warn(err);

            setTimeout(function () { // todo: передалать весь модуль нах
                isStarted = false;
            }, 50);

            visualisation._hideProgressInfo();

            $rootScope.$broadcast('ErrorMsg', [err.code, err.msg]);
        });

        $rootScope.$on(_msg.on_file_upload_progress, function (e, args) {
            if (isStarted && visualisation.active()) {
                visualisation._updateProgress.apply(visualisation, args);
            }
        });

        $rootScope.$on(_msg.on_file_upload_aborted, function (e, args) {
            if (isStarted && visualisation.active()) {
                var uin = args[0];

                if (fileUploader) {
                    fileUploader.abortUploading();
                } else {
                    visualisation.abortTransfer(uin);

                    // isStarted = false;
                    // uinDirect = 0;
                }

                if (uin) {
                    $rootScope.$broadcast(window._messages_.chatFrame.addCustomMSG, [{
                        UIN: uin,
                        Msg: mcLang(584) // "584":"Передача файлов прервана",
                    }]);

                    webix.message(mcLang(584));

                    $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                        mcConst._CMD_.ce_file_direct_upload_abort
                    ]);
                }

                // setTimeout(function () { // todo: передалать весь модуль нах
                    isStarted = false;
                // }, 50);
            }
        });

        $rootScope.$on(_msg.on_file_upload_complete, function (e, args) {
            if (isStarted && visualisation.active()) {
                var info = args[0];

                visualisation._hideProgressInfo(true);

                fileUploader.sendFileMessage(info);

                setTimeout(function () { // todo: передалать весь модуль нах
                    isStarted = false;
                }, 50);
            }
        });

        $rootScope.$on(_msg.on_file_upload_prepare_start, function () {
            if (!isStarted && canStart && visualisation.active()){
                visualisation._showProgressInfo(visualisation.constants.PREPARING);

                isStarted = true;
            }
        });

        $rootScope.$on(_msg.on_file_upload_prepare_progress, function (e, args) {
            if (isStarted && visualisation.active()) {
                var percent = args[0].percent;

                visualisation._onPrepareProgress(percent);
            }
        });
    }

    // --- direct =============================================================

    if (direct) {
        // --- upload ---

        $rootScope.$on(_msg.changeStateForUser, function (e, args) {
            if (isStarted && visualisation.active() && (visualisation.getState() === visualisation.constants.UPLOADIRECT || visualisation.getState() === visualisation.constants.WAITANSWER)) {
                var uin   = args[0];
                var state = args[1];

                if (direct.getCurrentReceiveUIN() == uin && state == mcConst.states.offline) {
                    direct.clear();

                    $rootScope.$broadcast(window._messages_.chatWrapper.hideTool, [mcConst.dataModels.ReceiveFiles]);
                } else
                if (uinDirect == uin) {
                    visualisation._hideProgressInfo();
                }

                isStarted = false;
            }
        });

        $rootScope.$on(_msg.on_file_direct_upload_wait_accept, function (e, args) {
            if (!isStarted && visualisation.active()) {
                visualisation._showProgressInfo(visualisation.constants.WAITANSWER);
            }
        });

        $rootScope.$on(_msg.on_file_direct_upload_accept, function (e, args) {
            if (!isStarted && visualisation.active() && visualisation.getState() === visualisation.constants.WAITANSWER) {
                var info = args[0];

                visualisation._showProgressInfo(visualisation.constants.UPLOADIRECT);

                isStarted = true;

                $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                    mcConst._CMD_.ce_file_direct_upload_start,
                    info
                ]);

                if (!$rootScope.isWebClient) {
                    $rootScope.$broadcast(window._messages_.mcStatistics.setStatValue, ["n_file_direct", 1, false]); // false - увеличить значение на "1"
                }
            }
        });

        $rootScope.$on(_msg.on_file_direct_upload_complete, function (e, args) {
            if (isStarted && visualisation.active() && uinDirect) {
                var files = args[0];

                $rootScope.$broadcast(window._messages_.chatFrame.addCustomMSG, [{
                    MsgType: mcConst._CMD_.msgType.SHOW_UPLOADED_FILES,
                    UIN: uinDirect,
                    FilesList: files.FilesList,
                    Count: files.FilesCount,
                    Size: files.FilesSize,
                    Path: files.filesPath
                }]);

                visualisation._hideProgressInfo(true);

                isStarted = false;
            }
        });

        $rootScope.$on(_msg.sendFilesToUser, function (e, args) {
            if (!isStarted && visualisation.active()) {
                _sendFilesToUser.apply(null, args);
            }
        });

        // --- download ---

        $rootScope.$on(_msg.acceptReceiveFiles, function (e, args) {
            if (!isStarted && visualisation.active()){
                direct.accept.apply(direct, args); // isStarted = true in onAccept

                if (!$rootScope.isWebClient) {
                    $rootScope.$broadcast(window._messages_.mcStatistics.setStatValue, ["n_frcvd", 1, false]); // false - увеличить значение на "1"
                }
            }
        });

        $rootScope.$on(_msg.rejectReceiveFiles, function () {
            if (isStarted && visualisation.active()) {
                direct.reject();

                isStarted = false;
            }
        });

        $rootScope.$on(_msg.on_file_request_transfer, function (e, args) {
            if (!isStarted && visualisation.active()) {
                visualisation._showProgressInfo(visualisation.constants.DOWNLOADPREP, true);

                visualisation._showDownloadFileList.apply(visualisation, args);
            }
        });

        $rootScope.$on(_msg.on_file_direct_upload_no_files, function () {
            if (!isStarted && visualisation.active()) {
                visualisation._hideProgressInfo(true);

                isStarted = false
            }
        });

        $rootScope.$on(_msg.on_file_direct_download_complete, function () {
            if (visualisation.active()) {
                var files = direct.getInfo();

                $rootScope.$broadcast(window._messages_.chatFrame.addCustomMSG, [{
                    MsgType: mcConst._CMD_.msgType.SHOW_RECEIVED_FILES,
                    UIN: files.UIN,
                    FilesList: files.FilesList,
                    Count: files.Count,
                    Size: files.Size,
                    Path: files.DownloadPath
                }]);

                visualisation._hideProgressInfo(true);

                isStarted = false;
                uinDirect = 0;
            }
        });

        $rootScope.$on(_msg.on_file_direct_download_file_received, function (e, args) {
            if (isStarted && visualisation.active()) {
                direct.addReceiveFile.apply(null, args);
            }
        });

        $rootScope.$on(_msg.on_file_direct_receive_client_disconnected, function (e, args) {
            if (isStarted && visualisation.active() && direct.getCurrentReceiveUIN() === args[0]) {
                visualisation.abortTransfer(args[1]);

                isStarted = false;
                uinDirect = 0;
            }
        });
    }
}

/*
 * smoothscroll polyfill - v0.3.5
 * https://iamdustan.github.io/smoothscroll
 * 2016 (c) Dustan Kasten, Jeremias Menichelli - MIT License
 */

(function(w, d, undefined) {
  'use strict';

  /*
   * aliases
   * w: window global object
   * d: document
   * undefined: undefined
   */

  // polyfill
  function polyfill() {
    // return when scrollBehavior interface is supported
    if ('scrollBehavior' in d.documentElement.style) {
      return;
    }

    /*
     * globals
     */
    var Element = w.HTMLElement || w.Element;
    var SCROLL_TIME = 468;

    /*
     * object gathering original scroll methods
     */
    var original = {
      scroll: w.scroll || w.scrollTo,
      scrollBy: w.scrollBy,
      elScroll: Element.prototype.scroll || scrollElement,
      scrollIntoView: Element.prototype.scrollIntoView
    };

    /*
     * define timing method
     */
    var now = w.performance && w.performance.now
      ? w.performance.now.bind(w.performance) : Date.now;

    /**
     * changes scroll position inside an element
     * @method scrollElement
     * @param {Number} x
     * @param {Number} y
     */
    function scrollElement(x, y) {
      this.scrollLeft = x;
      this.scrollTop = y;
    }

    /**
     * returns result of applying ease math function to a number
     * @method ease
     * @param {Number} k
     * @returns {Number}
     */
    function ease(k) {
      return 0.5 * (1 - Math.cos(Math.PI * k));
    }

    /**
     * indicates if a smooth behavior should be applied
     * @method shouldBailOut
     * @param {Number|Object} x
     * @returns {Boolean}
     */
    function shouldBailOut(x) {
      if (typeof x !== 'object'
            || x === null
            || x.behavior === undefined
            || x.behavior === 'auto'
            || x.behavior === 'instant') {
        // first arg not an object/null
        // or behavior is auto, instant or undefined
        return true;
      }

      if (typeof x === 'object'
            && x.behavior === 'smooth') {
        // first argument is an object and behavior is smooth
        return false;
      }

      // throw error when behavior is not supported
      throw new TypeError('behavior not valid');
    }

    /**
     * finds scrollable parent of an element
     * @method findScrollableParent
     * @param {Node} el
     * @returns {Node} el
     */
    function findScrollableParent(el) {
      var isBody;
      var hasScrollableSpace;
      var hasVisibleOverflow;

      do {
        el = el.parentNode;

        // set condition variables
        isBody = el === d.body;
        hasScrollableSpace =
          el.clientHeight < el.scrollHeight ||
          el.clientWidth < el.scrollWidth;
        hasVisibleOverflow =
          w.getComputedStyle(el, null).overflow === 'visible';
      } while (!isBody && !(hasScrollableSpace && !hasVisibleOverflow));

      isBody = hasScrollableSpace = hasVisibleOverflow = null;

      return el;
    }

    /**
     * self invoked function that, given a context, steps through scrolling
     * @method step
     * @param {Object} context
     */
    function step(context) {
      var time = now();
      var value;
      var currentX;
      var currentY;
      var elapsed = (time - context.startTime) / SCROLL_TIME;

      // avoid elapsed times higher than one
      elapsed = elapsed > 1 ? 1 : elapsed;

      // apply easing to elapsed time
      value = ease(elapsed);

      currentX = context.startX + (context.x - context.startX) * value;
      currentY = context.startY + (context.y - context.startY) * value;

      context.method.call(context.scrollable, currentX, currentY);

      // scroll more if we have not reached our destination
      if (currentX !== context.x || currentY !== context.y) {
        w.requestAnimationFrame(step.bind(w, context));
      }
    }

    /**
     * scrolls window with a smooth behavior
     * @method smoothScroll
     * @param {Object|Node} el
     * @param {Number} x
     * @param {Number} y
     */
    function smoothScroll(el, x, y) {
      var scrollable;
      var startX;
      var startY;
      var method;
      var startTime = now();

      // define scroll context
      if (el === d.body) {
        scrollable = w;
        startX = w.scrollX || w.pageXOffset;
        startY = w.scrollY || w.pageYOffset;
        method = original.scroll;
      } else {
        scrollable = el;
        startX = el.scrollLeft;
        startY = el.scrollTop;
        method = scrollElement;
      }

      // scroll looping over a frame
      step({
        scrollable: scrollable,
        method: method,
        startTime: startTime,
        startX: startX,
        startY: startY,
        x: x,
        y: y
      });
    }

    /*
     * ORIGINAL METHODS OVERRIDES
     */

    // w.scroll and w.scrollTo
    w.scroll = w.scrollTo = function() {
      // avoid smooth behavior if not required
      if (shouldBailOut(arguments[0])) {
        original.scroll.call(
          w,
          arguments[0].left || arguments[0],
          arguments[0].top || arguments[1]
        );
        return;
      }

      // LET THE SMOOTHNESS BEGIN!
      smoothScroll.call(
        w,
        d.body,
        ~~arguments[0].left,
        ~~arguments[0].top
      );
    };

    // w.scrollBy
    w.scrollBy = function() {
      // avoid smooth behavior if not required
      if (shouldBailOut(arguments[0])) {
        original.scrollBy.call(
          w,
          arguments[0].left || arguments[0],
          arguments[0].top || arguments[1]
        );
        return;
      }

      // LET THE SMOOTHNESS BEGIN!
      smoothScroll.call(
        w,
        d.body,
        ~~arguments[0].left + (w.scrollX || w.pageXOffset),
        ~~arguments[0].top + (w.scrollY || w.pageYOffset)
      );
    };

    // Element.prototype.scroll and Element.prototype.scrollTo
    Element.prototype.scroll = Element.prototype.scrollTo = function() {
      // avoid smooth behavior if not required
      if (shouldBailOut(arguments[0])) {
        original.elScroll.call(
            this,
            arguments[0].left || arguments[0],
            arguments[0].top || arguments[1]
        );
        return;
      }

      var left = arguments[0].left;
      var top = arguments[0].top;

      // LET THE SMOOTHNESS BEGIN!
      smoothScroll.call(
          this,
          this,
          typeof left === 'number' ? left : this.scrollLeft,
          typeof top === 'number' ? top : this.scrollTop
      );
    };

    // Element.prototype.scrollBy
    Element.prototype.scrollBy = function() {
      var arg0 = arguments[0];

      if (typeof arg0 === 'object') {
        this.scroll({
          left: arg0.left + this.scrollLeft,
          top: arg0.top + this.scrollTop,
          behavior: arg0.behavior
        });
      } else {
        this.scroll(
          this.scrollLeft + arg0,
          this.scrollTop + arguments[1]
        );
      }
    };

    // Element.prototype.scrollIntoView
    Element.prototype.scrollIntoView = function() {
      // avoid smooth behavior if not required
      if (shouldBailOut(arguments[0])) {
        original.scrollIntoView.call(
          this,
          arguments[0] === undefined ? true : arguments[0]
        );
        return;
      }

      // LET THE SMOOTHNESS BEGIN!
      var scrollableParent = findScrollableParent(this);
      var parentRects = scrollableParent.getBoundingClientRect();
      var clientRects = this.getBoundingClientRect();

      if (scrollableParent !== d.body) {
        // reveal element inside parent
        smoothScroll.call(
          this,
          scrollableParent,
          scrollableParent.scrollLeft + clientRects.left - parentRects.left,
          scrollableParent.scrollTop + clientRects.top - parentRects.top
        );
        // reveal parent in viewport
        w.scrollBy({
          left: parentRects.left,
          top: parentRects.top,
          behavior: 'smooth'
        });
      } else {
        // reveal element in viewport
        w.scrollBy({
          left: clientRects.left,
          top: clientRects.top,
          behavior: 'smooth'
        });
      }
    };
  }

  if (typeof exports === 'object') {
    // commonjs
    module.exports = { polyfill: polyfill };
  } else {
    // global
    polyfill();
  }
})(window, document);

function mcUploadProgress() {
    webix.type(webix.ui.list, {
        name: "myUploader",
        template: function (data) {
            var current_size = mcService.formatFileSize((data.size / 100) * data.percent);

            return mcService.myReplaceFormated(
                "<div class='uploader_overall'>" +
                "   <div class='uploader_status'>" +
                "       <div class='uploader_progress' style='width:#{width}'></div>" +
                "       <div class='uploader_message'>#{txt}</div>" +
                "   </div>" +
                "</div>",
                {
                    name: data.name,
                    width: data.percent + "%",
                    txt: current_size + (data.sizetext ? (" / " + data.sizetext + " ") : " ") + data.percent + "%"
                }
            );
        },
        status: function (f) {
            var messages = {
                server: f.sizetext + " - Done!",
                error: "Error",
                client: "Ready",
                transfer: f.sizetext
            };
            return messages[f.status];

        },
        height: 35
    });
}
function mcDialogsList($rootScope, mcPlaySound){
    "use strict";

    var ignoreList  = {};
    var statuses    = new MCStatusesList($rootScope);
    // var historyList = new MCHistoryDialogsList($rootScope);
    var privateList = new MCPrivatesList($rootScope, drawMessage, /*historyList,*/ statuses);
    // var confList    = new MCConferenceList($rootScope, drawMessage, statuses, mcPlaySound);

    // =================================================

    function drawMessage(msg){ // noUpdateCounter
        var currentID   = $rootScope.GetChatID();
        var currentType = $rootScope.GetChatType();
        // var uid         = msg.UID;
        var read        = false

        if (//(uid && !confList.hasMessage(uid, msg.Idx)) ||
            //(!uid && (
                (msg.UINFrom && privateList.hasPrivate(msg.UINFrom) && !privateList.hasMessage(msg.UINFrom, msg.Idx)) ||
                (msg.UINTo   && privateList.hasPrivate(msg.UINTo)   && !privateList.hasMessage(msg.UINTo, msg.Idx))
            //))
        ) { // чтобы не отображались офлайн сообщения, которые уже есть в чате

            $rootScope.$broadcast(window._messages_.chatFrame.addChatMessage, [msg]);

            // =========== CONFERENCE ===============
            // if (msg.UID){
            //     read = (currentID == msg.UID && currentType === 'UID') && mcConst.isWindowFocused;
            //
            //     if (confList.addMessage(uid, msg.Idx)){
            //         $rootScope.SendCMDToServer([
            //             mcConst._CMD_.cs_conf_got_message,
            //             mcConst.SessionID,
            //
            //             msg.UID,
            //             msg.Idx
            //         ]);
            //     }
            //
            //     if (read && msg.Idx >= confList.getReadIdx(msg.UID)) {
            //         $rootScope.SendCMDToServer([
            //             mcConst._CMD_.cs_conf_read_message,
            //             mcConst.SessionID,
            //
            //             msg.UID,
            //             msg.Idx
            //         ]);
            //     }
            // } else

            // =========== PRIVATES =================
            if (privateList.hasPrivate(msg.UINFrom)){
                read = (currentID == msg.UINFrom && currentType === 'UIN') && mcConst.isWindowFocused;

                if (privateList.addMessage(msg.UINFrom, msg.Idx, read) &&
                    (!(currentID == msg.UINFrom && currentType === 'UIN') || !(mcConst.isWindowFocused)) ) {
                    
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_private_msg_got,
                        mcConst.SessionID,

                        msg.UINFrom,
                        msg.Idx
                    ]);
                }

                if (currentID == msg.UINFrom && currentType === 'UIN' && msg.Idx >= privateList.getReadIdx(currentID) && read){
                    
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_private_msg_read,
                        mcConst.SessionID,

                        currentID,
                        msg.Idx
                    ]);
                }
            } else

            if (privateList.hasPrivate(msg.UINTo)){
                privateList.addMessage(msg.UINTo, parseInt(msg.Idx), msg.UINFrom == mcConst.UserInfo.UIN);
            }

            ///////----- -----///////

            // if (!msg.History && !msg.UpSync){
            //     msg.State = privateList.statuses.get(msg.UIN || msg.UINFrom);
            //
            //     if (([
            //             $rootScope.chatAliases.Kanban,
            //             $rootScope.chatAliases.BBS,
            //             $rootScope.chatAliases.SendBroadcast
            //         ].indexOf(currentType) >= 0) || (currentType === 'UID' && currentID != msg.UID && !noUpdateCounter)){
            //
            //         $rootScope.$broadcast('updateCounter', [msg]);
            //     } else
            //     if ( (currentType === 'UIN' && msg.UINFrom != mcConst.UserInfo.UIN && !noUpdateCounter) &&
            //         ( (currentID != msg.UINFrom && currentID != msg.UINTo) ||
            //         (msg.UID != undefined && currentType === 'UID' && currentID != msg.UID)) ){
            //
            //         $rootScope.$broadcast('updateCounter', [msg]);
            //     }
            // }
        }
    }

    function changeUserStatuses(_state){
        if (mcConst.LoggedIn){
            var Statuses     = mcService.getStateStatuses(_state);
            var myOfflineIdx = mcService.inArrayNoStrict(mcConst.UserInfo.UIN, Statuses.Offline);

            var UID = $rootScope.GetChatType() == 'UID' ? $rootScope.GetChatID() : -1;

            if (myOfflineIdx != -1) {
                Statuses.Offline.splice(myOfflineIdx, 1);
            }

            for (var i = 0; i < Statuses.Online.length; i ++){
                privateList.setState(Statuses.Online[i], Statuses.States[i]);
            }

            for (var i = 0; i < Statuses.Offline.length; i ++){
                privateList.setState(Statuses.Offline[i], mcConst.states.offline);

                // confList.getConfList().forEach(function (uid) {
                //     var id = mcService.findItemInArrayOfObj(confList.getConfUsers(uid), Statuses.Offline[i], 'UIN');
                //
                //     if (id >= 0 ) {
                //         confList.removeUsersFromConf(uid, Statuses.Offline[i]);
                //     }
                // });
            }

            if (Statuses.Online && Statuses.Online.length > 0){
                Statuses.Online.forEach(function (uin, id) {
                    if (Statuses.States[id] !== undefined){
                        if (!privateList.statuses.set(uin, Statuses.States[id])){
                            statuses.setUserState(uin, Statuses.States[id]);
                        }
                    }
                });
            }

            if (Statuses.Offline){
                Statuses.Offline.forEach(function (uin) {
                    if (!privateList.statuses.set(uin, mcConst.states.offline)){
                        statuses.setUserState(uin, mcConst.states.offline);
                    }
                });
            }

            // $rootScope.$broadcast('allUsersOnlineStatusesChanged', [statuses.getAllState()]);
        }
    }

    function getIgnoreList(list){
        list = list.Data.split(mcConst.terminator);

        var count = list.shift();
        var uin, nick, ignores;
        var res = {};

        while (count > 0){
            uin = list.shift();
            nick = list.shift();
            ignores = list.shift();

            res[uin] = {
                UIN: uin,
                DisplayName: nick,
                Ignores: ignores
            };

            count --;
        }

        return res;
    }

    function openPrivate(info){
        var isOpen = privateList.isOpen(info.UIN);

        privateList.addPrivate(info);

        if (info.UIN == 0){
            $rootScope.$broadcast('selectTool');
        }

        if (info.HelloMsg && !isOpen) {
            $rootScope.$broadcast('addCustomMSG', [{
                UIN : info.UIN,
                Text: info.HelloMsg
            }]);
        }
    }

    // function confKickUser(msg, tech_kick) {
    //     if (msg.UIN == mcConst.UserInfo.UIN){
    //         if (msg.History){
    //             // $rootScope.$broadcast('addCustomMSG', [mcService.Marge(msg, {
    //             //     noNotify: true,
    //             //     Text    : mcLang(550, [confList.getConfName(msg.UID), msg.ModerDisplayName]), // "550":"Вас изгнали из текстовой конференции \"%s\", модератор \"%s\"",
    //             // })]);
    //         } else {
    //             // if (!tech_kick) {
    //             //     webix.message(mcLang(550, [confList.getConfName(msg.UID), msg.ModerDisplayName])); // "550":"Вас изгнали из текстовой конференции \"%s\", модератор \"%s\"",
    //             // }
    //
    //             // confList.removeUsersFromConf(msg.UID, msg.UIN);
    //         }
    //     } else {
    //         if (msg.History){
    //             $rootScope.$broadcast('addCustomMSG', [mcService.Marge(msg, {
    //                 noNotify: true,
    //                 Text    : mcLang(551, [msg.ModerDisplayName, msg.UserDisplayName]), // "551":"Пользователь \"%s\" выгнал пользователя \"%s\" из конференции.",
    //             })]);
    //         } else {
    //             $rootScope.$broadcast('addCustomMSG', [mcLang(551, [msg.ModerDisplayName, msg.UserDisplayName])]); // "551":"Пользователь \"%s\" выгнал пользователя \"%s\" из конференции.",
    //
    //             confList.removeUsersFromConf(msg.UID, msg.UIN);
    //         }
    //     }
    // }

    // =================================================

    var _msg = window._messages_.dialogsList = {
        getConfHistory          : 'getConfHistory',
        getQuickUserInfo        : 'getQuickUserInfo',
        getUserProfile          : 'getUserProfile',
        OpenConf                : 'OpenConf',
        OpenPrivate             : 'OpenPrivate',
        OpenPrivateRequest      : 'OpenPrivateRequest',
        OnPrivateMessage        : 'OnPrivateMessage',
        OnConfMessage           : 'OnConfMessage',
        addNewConfData          : 'addNewConfData',
        confUserList            : 'confUserList',
        confLeaveUser           : 'confLeaveUser',
        confKickUser            : 'confKickUser',
        onlineOfflineUsers      : 'onlineOfflineUsers',
        getConfUsersList        : 'getConfUsersList',
        leaveConf               : 'leaveConf',
        onConfHistory           : 'onConfHistory',
        getPrivateHistory       : 'getPrivateHistory',
        getIgnoreList           : 'getIgnoreList',
        getUserIgnoreInfo       : 'getUserIgnoreInfo',
        getUserState            : 'getUserState',
        loadUserStatuses        : 'loadUserStatuses',
        getLastUserStates       : 'getLastUserStates',
        removeDialogFromList    : 'removeDialogFromList',
        loadPrivateDialogsList  : 'loadPrivateDialogsList',
        // loadHistoryDialogsList  : 'loadHistoryDialogsList',
        historyRemoveUser       : 'historyRemoveUser',
        updateSmallUserInfo     : 'updateSmallUserInfo',
        // OnOfflineMessage     : 'OnOfflineMessage',
        changeUserState         : 'changeUserState',
        getHistoryMsgRange      : 'getHistoryMsgRange',
        drawHistoryMessages     : 'drawHistoryMessages',
        clearConfOnReconnect    : 'clearConfOnReconnect',
        clearPrivatesOnReconnect: 'clearPrivatesOnReconnect',
        getFirstLastMsg         : 'getFirstLastMsg',
        getMyMessagesState      : 'getMyMessagesState',
        on_private_read_notify  : 'on_private_read_notify',
        on_private_got_notify   : 'on_private_got_notify',
        confUpdateUserInfo      : 'confUpdateUserInfo',
        getUserPhoto            : 'getUserPhoto',
        getActiveProcess        : 'getActiveProcess',
        clearConfHistory        : 'clearConfHistory',
        clearPrivHistory        : 'clearPrivHistory',
        setPrivatDialogsIdx     : 'setPrivatDialogsIdx',
        on_mainWindowFocused    : 'on_mainWindowFocused',
        getConfHistoryMsg       : 'getConfHistoryMsg',
        getPrivatHistoryMsg     : 'getPrivatHistoryMsg',

        '----':'----'
    };

    $rootScope.$on(_msg.on_mainWindowFocused, function(){
        var type = $rootScope.GetChatType();

        if (type === $rootScope.chatAliases.UIN){
            privateList.setAllMsgRead($rootScope.GetChatID());
        // } else
        //
        // if (type === $rootScope.chatAliases.UID){
        //     confList.setAllMsgRead($rootScope.GetChatID());
        }
    });

    // --- Privates ---

    $rootScope.$on(_msg.setPrivatDialogsIdx, function(e, args){
        privateList.setDialogIdx.apply(null, args);
    });

    $rootScope.$on(_msg.clearPrivHistory, function(e, args){
        privateList.clearHistory.apply(null, args);
    });

    $rootScope.$on(_msg.updateSmallUserInfo, function(e, args){
        var info = args[0];
        var uin  = info.UIN;

        if (info.Nick) {
            info.DisplayName = info.Nick;
        }

        privateList.updateUserInfo(uin, info);
    });

    $rootScope.$on(_msg.OpenPrivate, function(e, args){
        var uin = args[0].toString();

        // $rootScope.$broadcast('hide' + mcConst.dataModels.BBS);
        // $rootScope.$broadcast('hide' + mcConst.dataModels.SendBroadcast);
        // $rootScope.$broadcast('hide' + mcConst.dataModels.Forum);
        // $rootScope.$broadcast('hide' + mcConst.dataModels.Admin);
        // $rootScope.$broadcast('hide' + mcConst.dataModels.Kanban);

        if (uin != mcConst.UserInfo.UIN){
            if (privateList.hasPrivate(uin) && privateList.isOpen(uin)){
                privateList.showPrivate(uin);
            } else

            if (uin){
                $rootScope.$broadcast('SendCMDToServer' , [
                    mcConst._CMD_.cs_private_request,
                    mcConst.SessionID,
                    uin,
                    mcConst.Lang
                ]);
            }
        }
    });

    $rootScope.$on(_msg.OpenPrivateRequest, function (e, args) {
        openPrivate.apply(null, args);
    });

    $rootScope.$on(_msg.on_private_got_notify, function (e, args) {
        var UIN = args[0].UIN;
        var ID  = args[0].ID;

        privateList.setMyMsgGot(UIN, ID);
    });

    $rootScope.$on(_msg.on_private_read_notify, function (e, args) {
        var UIN = args[0].UIN;
        var ID  = args[0].ID;

        privateList.setMyMsgRead(UIN, ID);
    });

    $rootScope.$on(_msg.OnPrivateMessage, function(e, args){
        privateList.sendMessage.apply(null, args);
    });

    $rootScope.$on(_msg.getMyMessagesState, function(e, args){
        var uin = args[0];

        return privateList.getMyMsgReadState(uin);
    });

    $rootScope.$on(_msg.getPrivateHistory, function(e, args){
        privateList.getPrivateHistory.apply(null, args);
    });

    $rootScope.$on(_msg.clearPrivatesOnReconnect, function(e, args){
        privateList.clearPrivates.apply(null, args);
    });

    $rootScope.$on(_msg.getQuickUserInfo, function(e, args){
        var uin = args[0];
        var cb = args[1];

        if (cb) {
            cb(privateList.getPrivateInfo(uin));
        }
    });

    $rootScope.$on(_msg.getUserProfile, function(e, args){
        privateList.getUserProfile.apply(null, args);
    });

    $rootScope.$on(_msg.getActiveProcess, function(e, args){
        privateList.getActiveProcess.apply(null, args);
    });

    $rootScope.$on(_msg.loadPrivateDialogsList, function(){
        // if (mcConst.UserInfo.PvDlgIdx) {
            privateList.getDialogsList();
        // }
    });

    $rootScope.$on(_msg.getHistoryMsgRange, function(e, args){
        var id   = args[0];
        var type = args[1];

        if (type === $rootScope.chatAliases.UIN){
            privateList.getHistoryMsgRange(id, true);
        // } else {
        //     confList.getConfHistory(id, mcConst.countHistoryMessagesLoad, true);
        }
    });

    $rootScope.$on(_msg.drawHistoryMessages, function(e, args){
        privateList.drawHistoryMessages.apply(null, args);
    });

    $rootScope.$on(_msg.getFirstLastMsg, function(e, args){
        var uin   = args[0];
        var first = args[1];

        return first ? privateList.getFirstIdx(uin) : privateList.getLastIdx(uin);
    });

    $rootScope.$on(_msg.getUserPhoto, function(e, args){
        return privateList.getUserPhoto.apply(null, args);
    });

    $rootScope.$on(_msg.getPrivatHistoryMsg, function(e, args){
        return privateList.getHistoryMsg.apply(null, args);
    });

    // --- Channels ---

    // $rootScope.$on(_msg.getConfHistory, function(e, args){
    //     confList.getConfHistory.apply(null, args);
    // });
/*

    $rootScope.$on(_msg.clearConfHistory, function(e, args){
        confList.clearHistory.apply(null, args);
    });

    $rootScope.$on(_msg.OpenConf, function(e, args){
        // $rootScope.$broadcast('hide' + mcConst.dataModels.BBS);
        // $rootScope.$broadcast('hide' + mcConst.dataModels.SendBroadcast);
        // $rootScope.$broadcast('hide' + mcConst.dataModels.Forum);
        // $rootScope.$broadcast('hide' + mcConst.dataModels.Admin);
        // $rootScope.$broadcast('hide' + mcConst.dataModels.Kanban);

        confList.showConf.apply(null, args);
    });

    $rootScope.$on(_msg.OnConfMessage, function(e, args){
        confList.confMessage.apply(null, args);
    });

    $rootScope.$on(_msg.addNewConfData, function(e, args){
        confList.addConf.apply(null, args);
    });

    $rootScope.$on(_msg.confUserList, function(e, args){
        confList.addUsersToConf.apply(null, args);
    });

    $rootScope.$on(_msg.confLeaveUser, function(e, args){
        confList.removeUsersFromConf(args[0].UID, args[0].UINS || args[0].UIN);
    });

    $rootScope.$on(_msg.confKickUser, function(e, args){
        confKickUser.apply(null, args);
    });

    $rootScope.$on(_msg.getConfUsersList, function(e, args){
        var uid = args[0];
        var cb  = args[1];

        if (cb){
            cb(confList.getConfUsers(uid));
        }
    });

    $rootScope.$on(_msg.leaveConf, function(e, args){
        confList.leaveConf.apply(null, args);
    });

    $rootScope.$on(_msg.onConfHistory, function(e, args){
        confList.showConfHistory.apply(null, args);
    });

    $rootScope.$on(_msg.clearConfOnReconnect, function () {
        confList.clearUserList();
    });

    $rootScope.$on(_msg.confUpdateUserInfo, function (e, args) {
        confList.updateUserInfo.apply(null, args);
    });

    $rootScope.$on(_msg.getConfHistoryMsg, function (e, args) {
        return confList.getHistoryMsg.apply(null, args);
    });
*/

    // --- History ---

    // $rootScope.$on(_msg.historyRemoveUser, function (e, args) {
    //     historyList.removeUser.apply(null, args);
    // });

    // $rootScope.$on(_msg.loadHistoryDialogsList, function () {
    //     historyList.load();
    // });

    // --- States ---

    $rootScope.$on(_msg.changeUserState, function (e, args) {
        var user = args[0];

        statuses.setUserState(user.UIN, user.State);

        privateList.statuses.set(user.UIN, user.State);
    });

    $rootScope.$on(_msg.getUserState, function(e, args){
        var uin = args[0];
        var cb  = args[1];

        if (cb) {
            cb(privateList.statuses.get(uin));
        } else {
            return privateList.statuses.get(uin);
        }
    });

    $rootScope.$on(_msg.loadUserStatuses, function(){
        statuses.loadUserStatuses();
    });

    /* - Not Used - */ $rootScope.$on(_msg.getLastUserStates, function(e, args){
        var cb = args[0];

        if (cb) {
            cb(privateList.statuses.getAll());
        }
    });

    $rootScope.$on(_msg.onlineOfflineUsers, function(e, args){
        changeUserStatuses.apply(null, args);
    });

    // --- Other ---

    $rootScope.$on(_msg.getIgnoreList, function(e, args){
        $rootScope.$broadcast("SendCMDToServer", [
            mcConst._CMD_.cs_get_ignores_list,
            mcConst.SessionID,
            
            function( data ){
                var cb = args[0];

                ignoreList = getIgnoreList(data);

                if (cb){
                    cb(ignoreList);
                }
            }
        ])
    });

    $rootScope.$on(_msg.getUserIgnoreInfo, function(e, args){
        var uin = args[0].toString();
        var cb  = args[1];
        var res = null;

        if (ignoreList[uin]){
            res = ignoreList[uin];
        }

        if (cb) {
            cb(res);
        }
    });

    $rootScope.$on(_msg.removeDialogFromList, function(e, args){
        var uin = args[0];

        // console.log('removeDialogFromList: ' + uin);

        if (uin !== void(0)){
            privateList.removePrivate(uin);

            $rootScope.$broadcast(window._messages_.dialogsCtrl.removePrivateFromDialogs, [uin]);
        }
    });
}

function mcCmdParser (mcConnect, $rootScope, mcPlaySound, mcStatistics){
    "use strict";

    var SendCMDToServer = {};
    var ProcessCMD      = {};
    var pCmd            = {};
    var eCmd            = {};
    var lastError       = 0;
    var callBackPool    = {};
    var _CMD_           = mcConst._CMD_;
    var electronHandler = null;
    var parseCmdPool    = [];
    var cmdInProgress   = false;

    // ===================================

    function __reconnect(clear) {
        $rootScope.restoreConnection = false;

        $rootScope.$broadcast('ReconnectLoop');

        $rootScope.$broadcast('ErrorMsg', [_CMD_.Errors.Disconnected, mcConst.ErrorText[_CMD_.Errors.Disconnected] + "<br>" + mcLang(539) + "<br><br>" + mcConst.loadGif, '', function(){ // "539":"Восстанавливаем связь!",
            if (clear){
                mcService.ClearSessionSettings(true, true);
            }

            location.search = "";
            location.reload();
        }, mcLang(540)]); // "540":"Прервать",
    }

    function __loadHistoryLists() {
        $rootScope.$broadcast(window._messages_.dialogsList.loadPrivateDialogsList, []);
        // $rootScope.$broadcast(window._messages_.dialogsList.loadHistoryDialogsList, []);
        $rootScope.$broadcast(window._messages_.dialogsList.loadUserStatuses, []);
        $rootScope.$broadcast(window._messages_.dialogsList.getIgnoreList, []);
    }

    function __onWindowFocus(){
        mcConst.isWindowFocused = true;

        $rootScope.$broadcast('on_mainWindowFocused', []);
    }

    function __onWindowBlur() {
        mcConst.isWindowFocused = false;
    }

    function __loginFinish(){
        // if (!($rootScope.customUserOptions instanceof UsersCustomOptions)){
        //     $rootScope.customUserOptions = new UsersCustomOptions($rootScope);
        //
        //     $rootScope.customUserOptions.load();
        // }

        __loadHistoryLists();

        // $rootScope.startUrlOptions();

        window.removeEventListener('focus', __onWindowFocus);
        window.removeEventListener('blur',  __onWindowBlur);

        window.addEventListener("focus", __onWindowFocus);
        window.addEventListener("blur",  __onWindowBlur);

        // $rootScope.$broadcast('SendCMDToServer' , [
        //     mcConst._CMD_.cs_get_msgtemplates,
        //     mcConst.SessionID,
        //
        //     ProcessCMD[_CMD_.sc_get_msgtemplates]
        // ]);

        if (mcStatistics && mcStatistics.loadStat) {
            mcStatistics.loadStat(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN);
            mcStatistics.setValue(mcStatistics.val.cl_v,   window.__serverVer, true);
            mcStatistics.setValue(mcStatistics.val.cl_lng, mcConst.Lang, true);
        }

        $rootScope.LoginIsFinished = true;

        $rootScope.$broadcast(window._messages_.dialogsList.OpenPrivate, [$rootScope.chatWith]);
    }

    function __runCMD(send, args){
        if (mcService.isArray(args)){
            var cmd     = args.shift();
            var process = send ? SendCMDToServer : ProcessCMD;

            if (process[cmd]){
                if (send) {
                    process[cmd].apply(null, args);
                } else

                if (cmdInProgress) {
                    parseCmdPool.push([send, [cmd].concat(args)]);
                } else {
                    cmdInProgress = true;

                    // console.log("ParseCMD: " + cmd);

                    process[cmd].apply(null, args);

                    if (parseCmdPool.length){
                        setTimeout(function (){
                            cmdInProgress = false;

                            __runCMD.apply(null, parseCmdPool.shift());
                        }, 1);
                    } else {
                        cmdInProgress = false;
                    }
                }
            } else {
                console.warn(mcService.myReplace(mcService.Lang(9), [send ? "SendCMDToServer" : "ProcessCMD", cmd])); // "10" : "%s: %s not found!"
            }
        } else {
            console.error(mcService.myReplace(mcService.Lang(8), [send ? "SendCMDToServer" : "ProcessCMD"]));       // "8" : "%s: нет входящих аргументов"
        }
    }

    // ===================================

    function customCallBack (cmd, data){
        if (callBackPool[cmd] && callBackPool[cmd].length > 0){

            callBackPool[cmd].shift()(data);

            return true;
        }
    }

    function initSocketIo() {
        return $rootScope.initSocketIo();
    }

    function destroySocketIo() {
        mcConst.confsGrants = {};
        
        return $rootScope.destroySocketIo();
    }

    function criticalError (err){
        return ((mcService.inArray(err, [3, 4, 14, 15,/* 17,*/ 21, 23, 24, 44, 60, 71, 79, 80, 81, 82, 92]) !== -1) && mcConst.LoggedIn) || !mcConst.LoggedIn;
    }

    function runError(numb, params){
        if ($rootScope.ErrorActions[numb]){
            $rootScope.ErrorActions[numb](params);
        } else {
            return false;
        }
    }

    // ===================================

    this.runCmd         = __runCMD;

    /**
     * @return {boolean}
     */
    this.MultiSender    = function (cmd, sID){
        var next = true;

        if ((mcConst.socketIO || !$rootScope.isWebClient) && !SendCMDToServer[cmd] && mcService.InObject(cmd, _CMD_)){
            var data = "";

            for (var i = 1; i < arguments.length; i++){
                data += arguments[i] + (i !== arguments.length-1 ?  mcConst.CR : "");
            }

            mcConnect.SendDataToServer({
                CMD  : cmd,
                Data : data
            });

            sendPing(sID);

            next = false;
        }

        return next;
    };

    /**
     * @return {boolean}
     */
    this.MultiParser    = function (cmd, data) {
        var res = true;

        if (pCmd.hasOwnProperty(cmd)){
            $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

            customCallBack(pCmd[cmd], mcService.StringToObj(data));

            res = false;
        }

        return res;
    };

    this.addCustomCbCMD = function (cmd) {
        var cb = arguments[arguments.length - 1];
        
        if (mcService.isFunction(cb)){
            if (!callBackPool[cmd]){
                callBackPool[cmd] = [];
            }

            callBackPool[cmd].push(cb);

            return true;
        }
    };

    this.shiftFirstCMD  = function (cmd){
        if (callBackPool[cmd]) {
            callBackPool[cmd].shift();

            if (!callBackPool[cmd].length) {
                delete  callBackPool[cmd];
            }
        }
    };

    // ====================================================================================

    pCmd[_CMD_.sc_dept_list]                      = _CMD_.cs_get_depts_list;
    pCmd[_CMD_.sc_web_user_foto_file]             = _CMD_.cs_web_get_user_foto_file;
    pCmd[_CMD_.sc_get_channels_list]              = _CMD_.cs_get_channels_list;
    pCmd[_CMD_.sc_ignores_list]                   = _CMD_.cs_get_ignores_list;
    pCmd[_CMD_.sc_online_users_states]            = _CMD_.cs_get_online_users_states;
    pCmd[_CMD_.sc_user_foto]                      = _CMD_.cs_get_uin_foto;
    pCmd[_CMD_.sc_file_exists]                    = _CMD_.cs_is_file_exists;
    pCmd[_CMD_.sc_image_thumbs]                   = _CMD_.cs_get_image_thumbs;
    pCmd[_CMD_.sc_serv_stat]                      = _CMD_.cs_stat;
    pCmd[_CMD_.sc_bbs_body]                       = _CMD_.cs_get_bbs;
    pCmd[_CMD_.sc_public_ftp_info]                = _CMD_.cs_get_public_ftp_info;
    pCmd[_CMD_.sc_apply_client_settings]          = _CMD_.cs_get_options_preset;
    pCmd[_CMD_.sc_sync_conf_history]              = _CMD_.cs_sync_conf_history;
    pCmd[_CMD_.sc_users_live_search]              = _CMD_.cs_users_live_search;
    pCmd[_CMD_.sc_private_dialogs]                = _CMD_.cs_get_private_dialogs;
    pCmd[_CMD_.sc_del_private_dialog ]            = _CMD_.cs_del_private_dialog;
    // pCmd[_CMD_.sc_private_read]                   = _CMD_.cs_private_msg_read;
    pCmd[_CMD_.sc_private_got]                    = _CMD_.cs_private_msg_got;
    pCmd[_CMD_.sc_private_dialog_msg_states]      = _CMD_.cs_private_get_dialog_msg_states;
    pCmd[_CMD_.sc_users_list_for_redirect_dialog] = _CMD_.cs_get_users_for_redirect_dialog;
    pCmd[_CMD_.sc_broadcast_sent_ok]              = _CMD_.cs_send_broadcast;
    pCmd[_CMD_.sc_user_active_window_caption]     = _CMD_.cs_get_active_window_caption;
    pCmd[_CMD_.sc_update_self_nick_avatar_sex]    = _CMD_.cs_set_uin_info;
    pCmd[_CMD_.sc_get_msgtemplates]               = _CMD_.cs_get_msgtemplates;
    pCmd[_CMD_.sc_user_computer_info]             = _CMD_.cs_get_user_computer_info;
    pCmd[_CMD_.sc_users_changeid_data]            = _CMD_.cs_get_users_changeid_data;
    pCmd[_CMD_.sc_find_users_list]                = _CMD_.cs_find_users;
    pCmd[_CMD_.sc_broadcasts_list_history]        = _CMD_.cs_get_broadcasts_history;
    pCmd[_CMD_.sc_user_current_time]              = _CMD_.cs_get_remote_uin_current_time;

    // ====================================================================================

    ProcessCMD[_CMD_.sc_login]                    = function sc_login(_data) {
        var hasDelayedCommands = mcConst.UserInfo.ChangeID !== -1 && mcConst.UserInfo.ChangeID !==_data.ChangeID;
        var oldChangeID        = hasDelayedCommands ? mcConst.UserInfo.ChangeID : 0;

        mcConst.UserInfo = mcService.Marge(mcConst.UserInfo, _data);
        
        mcConst.UserInfo.DisplayName       = mcConst.UserInfo.Nick;
        mcConst.UserInfo.MaxUploadFileSize = mcConst.UserInfo.MaxUploadFileSize * 1024 * 1024;

        if (!$rootScope.isWebClient && mcConst.UserInfo.OptionsPresetCRC32 && (mcConst.settingsCRC32 !== mcConst.UserInfo.OptionsPresetCRC32)) {
            mcConst.settingsCRC32 = mcConst.UserInfo.OptionsPresetCRC32;

            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_get_options_preset,
                mcConst.SessionID,
                mcConst.UserInfo.OptionsPresetID
            ]);
        }

        mcConst.UserInfo.State = mcConst.states.online;

        $rootScope.$broadcast(window._messages_.dialogsList.setPrivatDialogsIdx, [mcConst.UserInfo.PvDlgIdx]);
        $rootScope.$broadcast(window._messages_.usersSmallInfo.setUserSmallInfo, [{
            UIN         : mcConst.UserInfo.UIN,
            DisplayName : mcConst.UserInfo.DisplayName,
            ChangeID    : mcConst.UserInfo.ChangeID,
            Avatar      : mcConst.UserInfo.Avatar,
            Sex         : mcConst.UserInfo.Sex
        }]);

        mcConst.LoggedIn = true;

        mcService.SaveSessionSettings(mcConst.LoginInfo);

        $rootScope.$broadcast('hide' + mcConst.lockInterface, ["all"]); // unlock

        if (!$rootScope.isWebClient){
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_client_connected,

                mcConst.UserInfo
            ]);
        }

        if (hasDelayedCommands){
            setTimeout(function () {
                $rootScope.$broadcast('SendCMDToServer', [
                    mcConst._CMD_.cs_get_delayed_commands,
                    mcConst.SessionID,

                    oldChangeID
                ]);
            }, 100);
        }

        if (mcConst.UserInfo.State !== mcConst.states.dnd){
            $rootScope.$broadcast('SendCMDToServer' , [
                mcConst._CMD_.cs_set_state,
                mcConst.SessionID,

                mcConst.states.online
            ]);
        }

        if (!$rootScope.restoreConnection){
            mcConst.settingsCRC32 = mcConst.UserInfo.OptionsPresetCRC32;

            $rootScope.$broadcast('hide' + mcConst.dataModels.Login, []);
        }

        $rootScope.SendCMDToServer([
            mcConst._CMD_.cs_login_done,
            mcConst.SessionID
        ]);

        /*window.__sendPing = function () {
            var rnd = mcService.RandomHash(5);

            window.___native_console.time("Server ping (" + rnd + ") ");

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_ping,
                mcConst.SessionID,

                function () {
                    window.___native_console.timeEnd("Server ping (" + rnd + ") ");
                }
            ]);
        };
        window.__disconnect = function() {
            SendCMDToServer[_CMD_.cs_quit](mcConst.SessionID);
            ProcessCMD[_CMD_.sc_drop_connect]();
        }*/
    };

    ProcessCMD[_CMD_.sc_pong]                     = function() {
        customCallBack(_CMD_.cs_ping);
    };

    ProcessCMD[_CMD_.sc_login_finish]             = function() {
        console.log('Logged in!');

        __loginFinish();

        if (!$rootScope.isWebClient){
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [mcConst._CMD_.ce_login_finish, mcConst.UserInfo.AutoAwayTime]);
        }

        // $rootScope.checkNewVersion();
    };

    ProcessCMD[_CMD_.sc_registered]               = function sc_registered(data) {
        // "UIN"          : 98,              // присвоенный идентификатор пользователя
        // "Nick"         : "Chapay",        // ник
        // "Pass"         : "1@56#4sdfKDF",  // пароль учётной записи
        // "Avatar"       : 53,              // номер аватара
        // "Sex"          : 1,               // пол пользователя
        // "LoginStyle"   : 0,               // (5.14+) тип авторизации (0 - обычная, пара UIN/Email/Nick и пароль; 1 - AD, ник/домен; 2 - без пароля, только ник)
        // "AutoAwayTime" : 15,              // (5.14+) время в минутах, после которого будет считаться, что пользователь неактивен за компьютером, если он не нажимал ничего на клавиатуре и не шевелил мышкой
        // "Domain"       : "domainname",    // (5.14+) имя домена пользователя, если есть
        //
        // "OptionsPresetID"    : 17,                  // (5.17+) идентификатор название шаблона настроек, если 0 - шаблона нет
        // "OptionsPresetCRC32" : 4564871              // (5.17+) контрольная сумма шаблона настроек

        var info = mcService.StringToObj(data);

        if ($rootScope.isWebClient){
            $rootScope.$broadcast('StopPingTimer', []);
        }

        setTimeout(function () {
            SendCMDToServer[_CMD_.Login](info.UIN, mcConst.LoginInfo.pwd, mcConst.LoginInfo.servPwd, mcConst.LoginInfo.rm, mcConst.SessionID);
        }, 100);
    };

    ProcessCMD[_CMD_.sc_error]                    = function sc_error(_data) {
        var err = mcService.StringToObj(_data) || {
            "ErrNum" : 21,        // номер ошибки
            "Params" : [          // параметры ошибки, если есть, параметров может быть любое количество, все - текстовые строки
                "bla-bla-bla",
                "test-test-test"
            ]
        };
        err.ErrNum = parseInt(err.ErrNum);

        console.warn("ERR [" + err.ErrNum + "]: " + mcService.myReplace(mcService.Error(err.ErrNum), [].concat(err.Params)));

        lastError = err.ErrNum;

        if (criticalError(err.ErrNum) || ($rootScope.ErrorActions[err.ErrNum] && $rootScope.ErrorActions[err.ErrNum](err.Params))){
            $rootScope.$broadcast('StopPingTimer', []);
        }

        if (!mcConst.LoggedIn && $rootScope.loginFormNotShowed) {
            if ($rootScope.isWebClient){
                $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

                runError(err.ErrNum, err.Params);
            } else {
                if ($rootScope.restoreConnection) {
                    if (err.ErrNum === mcConst._CMD_.Errors.WrongServerVersion || err.ErrNum === mcConst._CMD_.Errors.ConnectionLimit){
                        $rootScope.$broadcast(window._messages_.main.hideAllErrors);
                        
                        $rootScope.$broadcast('ErrorMsg', [err.ErrNum, mcService.Error(err.ErrNum), err.Params, function(){
                            $rootScope.$broadcast('hide' + mcConst.lockInterface, ["all"]); // unlock

                            runError(err.ErrNum, err.Params);

                            $rootScope.UNIT.ctrlServersManager.setAutoConnectInfo("", false, "");

                            location.reload();
                        }]);
                    } else {
                        __reconnect();
                    }
                } else {
                    $rootScope.$broadcast('ErrorMsg', [err.ErrNum, mcService.Error(err.ErrNum), err.Params, function(){
                        $rootScope.$broadcast('hide' + mcConst.lockInterface, ["all"]); // unlock

                        runError(err.ErrNum, err.Params);

                        $rootScope.UNIT.ctrlServersManager.setAutoConnectInfo("", false, "");

                        if (!mcConst.LoggedIn){
                            $rootScope.$broadcast('show' + mcConst.dataModels.Login, []);
                        } else {
                            location.reload(); 
                        }
                    }]);
                }
            }
        } else

        if (err.ErrNum !== mcConst._CMD_.Errors.NetworkError || (!$rootScope.loginFormNotShowed && err.ErrNum === mcConst._CMD_.Errors.NetworkError)){ //    "224": "сетевая ошибка: %s"
            $rootScope.$broadcast('ErrorMsg', [err.ErrNum, mcService.Error(err.ErrNum), err.Params, function(){
                if (!mcConst.LoggedIn){
                    $rootScope.$broadcast('enable' + mcConst.dataModels.Login, []);
                } else
                if (criticalError(err.ErrNum)){
                    location.search = "";
                    location.reload();
                }

                $rootScope.$broadcast('hide' + mcConst.lockInterface, ['all']);

                runError(err.ErrNum, err.Params);

                $rootScope.$broadcast(window._messages_.chatFrame.focusToEnterChat);
            }]);
        }
    };

    ProcessCMD[_CMD_.sc_halt]                     = function() {
        $rootScope.$broadcast('StopPingTimer' ,[]);

        if ($rootScope.isWebClient){
            destroySocketIo();

            $rootScope.$broadcast('ErrorMsg', [_CMD_.Errors.Disconnected, mcService.Error(_CMD_.Errors.Disconnected), '', function(){
                location.search = "";
                location.reload();
            }]);
        } else {
            console.log("ProcessCMD[_CMD_.sc_halt]");

            $rootScope.$broadcast(window._messages_.clientData.quitFromProgram, true);
        }
    };

    ProcessCMD[_CMD_.sc_get_all_rights]           = function sc_get_all_rights(_data){
        var rSet = _data.RightsSet;
        var notHasRights = !mcConst.MyRightsSet.length;

        for (var i = 0; i < rSet.length; i++){
            mcConst.MyRightsSet[i] = (rSet[i] !== '0');
        }

        if (!$rootScope.restoreConnection && notHasRights){
            // $rootScope.$broadcast('show' + mcConst.dataModels.ChatWrapper, []);
            $rootScope.$broadcast('show' + mcConst.dataModels.ChatFrame, [mcConst.containers.center]);
        }

        $rootScope.checkAbilityByRights();
    };

    ProcessCMD[_CMD_.sc_drop_connect]             = function sc_drop_connect(){
        destroySocketIo();

        $rootScope.LoginIsFinished = false;

        if (!mcConst.LoggedIn && mcConst.PingTimer && !$rootScope.restoreConnection){
            $rootScope.$broadcast('StopPingTimer' ,[]);

            if (!$rootScope.isWebClient){
                if (mcConst.LoginInfo.rm && !$rootScope.restoreConnection){
                    $rootScope.$broadcast('show' + mcConst.dataModels.Login, [mcConst.Lang, true]);
                    $rootScope.$broadcast('hide' + mcConst.lockInterface, ["all"]); // unlock
                } else {
                    mcService.SaveSessionSettings(mcConst.LoginInfo.login, "", "", false);

                    $rootScope.$broadcast('hide' + mcConst.lockInterface, ["all"]); // unlock

                    mcConst.LoginInfo.pwd     = "";
                    mcConst.LoginInfo.servPwd = "";
                    mcConst.LoginInfo.rm      = false;

                    !$rootScope.drodConnectOnLogin && $rootScope.$broadcast('ErrorMsg', ['0', mcLang(517), '', function(){ // "517":"Нет соединения с сервером",
                        $rootScope.$broadcast('show' + mcConst.dataModels.Login, []);
                    }]);
                }
            } else {
                mcService.SaveSessionSettings(mcConst.LoginInfo.login, "", "", false);

                $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

                mcConst.LoginInfo.pwd     = "";
                mcConst.LoginInfo.servPwd = "";
                mcConst.LoginInfo.rm      = false;

                $rootScope.$broadcast('show' + mcConst.dataModels.Login, []);
            }
        } else

        if (mcConst.LoggedIn || mcConst.PingTimer) {
            $rootScope.$broadcast('hide' + mcConst.dataModels.Broadcast, []);
            $rootScope.$broadcast('StopPingTimer' ,[]);

            if ($rootScope.isWebClient){
                mcConst.LoginInfo.pwd     = "";
                mcConst.LoginInfo.servPwd = "";
                mcConst.LoginInfo.rm      = false;

                $rootScope.$broadcast('ErrorMsg', [_CMD_.Errors.Disconnected, mcService.Error(_CMD_.Errors.Disconnected), '', function(){
                    location.search = "";
                    location.reload();
                }]);
            } else {
                $rootScope.$broadcast('closeKanban' ,[]);

                __reconnect(true);
            }
        } else
            
        if (!$rootScope.isWebClient && ($rootScope.lastError == _CMD_.Errors.Disconnected || $rootScope.lastError == _CMD_.Errors.Disconnected)) {
            __reconnect();

            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [mcConst._CMD_.ce_client_disconnected]);
        }
    };

    ProcessCMD[_CMD_.Logs]                        = function(data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

        $rootScope.$broadcast('LogsChunk', [info]); // unlock
    };

    // ProcessCMD[_CMD_.sc_get_common_contacts_list] = function sc_get_common_contacts_list(data){
    //     var info = mcService.StringToObj(data);
    //
    //     $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock
    //
    //     if (!customCallBack(_CMD_.cs_get_common_contacts_list, info)) {
    //         $rootScope.$broadcast(window._messages_.CommonContacts.newCommonContactList, [info]);
    //     }
    // };

    ProcessCMD[_CMD_.sc_grant_private_access]     = function sc_grant_private_access(data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

        $rootScope.$broadcast('OpenPrivateRequest', [info]);
    };

    ProcessCMD[_CMD_.sc_private_info]             = function sc_private_info(data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

        customCallBack(_CMD_.cs_get_private_info, info);
    };

    ProcessCMD[_CMD_.sc_remove_conversations]     = function sc_private_info(data){
        var info = mcService.StringToObj(data);

        if (info.Private) {
            $rootScope.$broadcast(window._messages_.dialogsCtrl.clearAllDialogsMessages, []);
        }

        if (info.Conf) {
            $rootScope.$broadcast(window._messages_.dialogsCtrl.clearAllConfMessages, []);
        }
    };

    ProcessCMD[_CMD_.sc_join_txt_channel]         = function sc_join_txt_channel(data){
        var info = mcService.StringToObj(data);

        if (info.UID) {
            $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

            $rootScope.$broadcast(window._messages_.dialogsList.addNewConfData, [mcService.Marge({}, info)]);

            if (info.UsersList && info.UsersList.length) {
                $rootScope.$broadcast(window._messages_.usersSmallInfo.updateUsersSmallInfo, [
                    info.UsersList,

                    function (updatedUsersList) {
                        $rootScope.$broadcast(window._messages_.dialogsList.confUserList, [info.UID, updatedUsersList]);

                        customCallBack(_CMD_.cs_join_txt_channel, info);
                    }
                ]);
            }

            customCallBack(_CMD_.cs_create_txt_channel, info);
        }
    };

    // ProcessCMD[_CMD_.sc_txt_channel_created]      = function sc_private(data){
    //     var info = mcService.StringToObj(data);
    //
    //     customCallBack(_CMD_.cs_create_txt_channel, info);
    // };

    ProcessCMD[_CMD_.sc_private]                  = function sc_private(data){
        var info = mcService.StringToObj(data);

        if (info.MsgType !== mcConst._CMD_.msgType.COMMENT && (info.UINFrom == $rootScope.chatWith || info.UINFrom == mcConst.UserInfo.UIN)){
            $rootScope.$broadcast('OnPrivateMessage', [info]);
        } else {
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_web_support_i_am_busy,
                mcConst.SessionID,
                info.UINFrom,
                $rootScope.chatWith
            ]);
        }
    };

    // ProcessCMD[_CMD_.sc_put_msg2txt_channel]      = function sc_put_msg2txt_channel(data){
    //     var info = mcService.StringToObj(data);
    //
    //     $rootScope.$broadcast('OnConfMessage', [info]);
    // };

    // ProcessCMD[_CMD_.sc_join_newbies_txt_channel] = function sc_join_newbies_txt_channel(data){
    //     var users = mcService.StringToObj(data);
    //     var uid   = users.UID;
    //
    //     delete users.UID;
    //
    //     var usersList = mcService.convertObjToArray(users, 'UIN');
    //     var myUinIdx  = mcService.findItemInArrayOfObj(usersList, mcConst.UserInfo.UIN, "UIN");
    //
    //     if (myUinIdx !== -1){
    //         usersList.splice(myUinIdx, 1);
    //     }
    //
    //     if (usersList.length){
    //         $rootScope.$broadcast(window._messages_.usersSmallInfo.updateUsersSmallInfo, [
    //             usersList,
    //
    //             function (updatedUsersList) {
    //                 updatedUsersList.forEach(function (user) {
    //                     user.State = user.hasOwnProperty("State") ? user.State : mcConst.states.online;
    //
    //                     // $rootScope.$broadcast('addCustomMSG', [{
    //                     //     UID     : uid,
    //                     //     noNotify: true,
    //                     //     Text    : mcLang(650, [user.DisplayName]), // "650":"%s входит в конференцию",
    //                     // }]);
    //                 });
    //
    //                 $rootScope.$broadcast(window._messages_.dialogsList.confUserList, [uid, updatedUsersList]);
    //             }
    //         ]);
    //     }
    // };

    ProcessCMD[_CMD_.sc_online_notify]            = function sc_leave_txt_channel(data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.dialogsList.getQuickUserInfo, [info.UIN, function (user) {
            user && $rootScope.$broadcast('addCustomMSG', [{
                UIN     : user.UIN,
                inOnline: true,
                noBlink : true,
                Text    : mcLang(651, [user.DisplayName]), // "651":"%s в сети!",
            }]);
        }]);
    };

    // ProcessCMD[_CMD_.sc_leave_txt_channel]        = function sc_leave_txt_channel(data){
    //     var info = mcService.StringToObj(data);
    //
    //     $rootScope.$broadcast('confLeaveUser', [info]);
    // };

    // ProcessCMD[_CMD_.sc_kill_txt_ch]              = function sc_kill_txt_ch(data){
    //     var info = mcService.StringToObj(data);
    //
    //     $rootScope.$broadcast('confLeaveUser', [{
    //         UID: info.UID,
    //         UIN: mcConst.UserInfo.UIN
    //     }]);
    // };

    // ProcessCMD[_CMD_.sc_kick_from_txt_channel]    = function sc_kick_from_txt_channel(data){
    //     var info = mcService.StringToObj(data);
    //
    //     $rootScope.$broadcast('confKickUser', [info]);
    // };

    // ProcessCMD[_CMD_.sc_tech_kick]                = function sc_kick_from_txt_channel(data){
    //     var info = mcService.StringToObj(data);
    //
    //     info.UIN = mcConst.UserInfo.UIN;
    //
    //     $rootScope.$broadcast('confKickUser', [info, true]);
    // };

    ProcessCMD[_CMD_.sc_just_connect_disconnect_users] = function sc_just_connect_disconnect_users(data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('onlineOfflineUsers', [info]);
    };

    // ProcessCMD[_CMD_.sc_ulist_in_txt_ch]          = function sc_ulist_in_txt_ch(data){ // DEPRECATED
    //     var info = mcService.StringToObj(data);
    //     var uid  = info.shift();
    //
    //     $rootScope.$broadcast(window._messages_.dialogsList.confUserList, [uid, info]);
    // };

    ProcessCMD[_CMD_.sc_user_state]               = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.dialogsList.changeUserState, [info]);
    };

    ProcessCMD[_CMD_.sc_adm_command_complete]     = function (info){

        switch (info.Cmd){

            //ce_del_server

            default:
                customCallBack(info.Cmd);
        }
    };

    ProcessCMD[_CMD_.sc_apply_client_settings]    = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.Settings.changeUserProfile, [info]);
    };

    ProcessCMD[_CMD_.sc_private_read_notify]      = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast("on_private_read_notify", [info]);
    };

    ProcessCMD[_CMD_.sc_private_read]      = function (data){
        var info = mcService.StringToObj(data);

        if (!customCallBack(_CMD_.cs_private_msg_read, info)) {
            $rootScope.$broadcast("on_private_read_notify", [info]);
        }
    };

    ProcessCMD[_CMD_.sc_private_got_notify]       = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast("on_private_got_notify", [info]);
    };

    // ProcessCMD[_CMD_.sc_get_msgtemplates]         = function (data){
    //     var info = mcService.StringToObj(data);
    //
    //     mcConst.UserInfo.Templates = info;
    //
    //     $rootScope.$broadcast(window._messages_.chatFrame.quickMessagesList, [info]);
    // };

    ProcessCMD[_CMD_.sc_users_list]               = function(data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

        customCallBack(_CMD_.cs_get_users_list, info);
    };

    ProcessCMD[_CMD_.sc_caches]                   = function(data){
        mcConst.contactsCaches = mcService.StringToObj(data);
    };

    ProcessCMD[_CMD_.sc_txt_channel_give_grant]   = function(data){
        var info = mcService.StringToObj(data);

        mcConst.confsGrants[info.UID] = info;
    };

    ProcessCMD[_CMD_.sc_files_offline_exists]     = function(data){
        var info = mcService.StringToObj(data);

        mcConst.offlineFiles[info.UIN] = info;

        console.log("Offline files: " + JSON.stringify(info));
    };

    ProcessCMD[_CMD_.sc_private_tech_message]     = function (data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('addCustomMSG', [info]);
        // $rootScope.$broadcast('OnPrivateMessage', [info]);
    };

    ProcessCMD[_CMD_.sc_serv_stat]                = function (data) {
        var info = mcService.StringToObj(data);

        webix.ajax().post('http://mychat-server.com/stat.txt', JSON.stringify(info.Stat));
    };

    ProcessCMD[_CMD_.sc_web_services_info]        = function (data) {
        var info = mcService.StringToObj(data);

        // "AliasAdmin" : "admin", // папка-псевдоним, путь к админке
        // "AliasAPI" : "API", // папка-псевдоним, путь к Integration API
        // "AliasChat" : "chat", // папка-псевдоним, путь к WEB-чату
        // "AliasFiles" : "files", // папка-псевдоним, путь к файлам, залитым на сервер
        // "AliasForum" : "forum", // папка-псевдоним, путь ко встроенному форуму
        // "AliasKanban" : "kanban", // папка-псевдоним, путь к канбан-доске
        //
        // "LobbyEnable" : true, // включен доступ к списку сервисов MyChat
        // "ForumEnable" : true, // включить доступ ко встроенному форуму
        // "KanbanEnable" : true, // включить доступ к канбан-доске
        // "ChatEnable" : true // включить доступ к WEB-чату
        // "AdminEnable" : true // включить доступ к WEB-админке

        mcConst.pathAliases = info;

        if (!$rootScope.isWebClient){
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_web_services_info,

                info
            ]);
        }
    };

    ProcessCMD[_CMD_.sc_server_ports]             = function (data) {
        var info = mcService.StringToObj(data);

        // "PortCore" : 2004,  // TCP порт ядра сервера MyChat
        // "PortFTP"  : 20001, // TCP порт файлового сервера
        // "PortNode" : 8080,  // TCP порт для доступа к серверу NodeJS
        // "HTTPS"    : false  // используется ли шифрование трафика в NodeJS

        mcConst.UserInfo = mcService.Marge(mcConst.UserInfo, info);

        if (!$rootScope.isWebClient){
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_server_ports,

                info
            ]);
        }
    };
    
    ProcessCMD[_CMD_.sc_token_created]            = function (data) {
        var info = mcService.StringToObj(data);
        var url  = "ftp://" + info.Where + ":" + info.Token + "@" + mcService.createHostPath(null, mcConst.UserInfo.PortFTP, true);

        if (!customCallBack(_CMD_.cs_create_token, info) && !$rootScope.isWebClient){
            $rootScope.$broadcast('sendCMDToElectron', [
                mcConst._CMD_.ce_open_ftp_url,
                url
            ]);
        }
    };

    ProcessCMD[_CMD_.sc_change_server]            = function (data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.serversManager.addNewServerByConfig, [{
            ServHost: info.IP1,        // основной адрес сервера чата
            Port    : info.Port1,      // порт для подключения
            ServName: info.ServName,   // название для сервера (условно)
            ServPass: info.Pass,       // пароль для доступа к серверу (если указан на сервере, иначе - пустая строка)
            AltHost : info.IP2,        // резервный адрес сервера чата
            AltPort : info.Port2,      // второй порт для подключения
            Now     : info.Immediately // (8.0+) отключиться от сервера немедленно, если true, и соединиться заново, с новыми настройками
        }]);
    };

//     ProcessCMD[_CMD_.sc_conf_read]            = function (data) {
//         var info = mcService.StringToObj(data);
//
// /*
//         $rootScope.$broadcast(window._messages_.serversManager.addNewServerByConfig, [{
//             ServHost: info.IP1,        // основной адрес сервера чата
//             Port    : info.Port1,      // порт для подключения
//             ServName: info.ServName,   // название для сервера (условно)
//             ServPass: info.Pass,       // пароль для доступа к серверу (если указан на сервере, иначе - пустая строка)
//             AltHost : info.IP2,        // резервный адрес сервера чата
//             AltPort : info.Port2,      // второй порт для подключения
//             Now     : info.Immediately // (8.0+) отключиться от сервера немедленно, если true, и соединиться заново, с новыми настройками
//         }]);
// */
//     };

    // ProcessCMD[_CMD_.sc_close_broadcast_window]   = function () {
    //     $rootScope.$broadcast(window._messages_.Broadcast.closeReceiveBroadcast);
    // };

    // === MEDIA CALL ================================================

    ProcessCMD[_CMD_.sc_media_call]               = function media_call(data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_call', [info]);
    };

    ProcessCMD[_CMD_.sc_media_call_start_ok]               = function sc_media_call_start_ok(data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_call_start_ok', [info]);
    };

    ProcessCMD[_CMD_.sc_media_call_accept]        = function media_call_accept(data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_call_accept', [info]);
    };

    ProcessCMD[_CMD_.sc_media_call_reject]        = function media_call_reject(data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_call_reject', [info]);
    };

    ProcessCMD[_CMD_.sc_media_call_close]         = function media_call_close(data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_call_close', [info]);
    };

    ProcessCMD[_CMD_.sc_media_call_error]         = function media_call_error(data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_call_error', [info]);
    };

    ProcessCMD[_CMD_.sc_media_call_busy]          = function media_call_busy(data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_call_busy', [info]);
    };

    ProcessCMD[_CMD_.sc_media_ready]              = function (data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_ready', [info]);
    };

    ProcessCMD[_CMD_.sc_media_offer]              = function (data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_offer', [info]);
    };

    ProcessCMD[_CMD_.sc_media_answer]             = function (data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_answer', [info]);
    };

    ProcessCMD[_CMD_.sc_media_ice_candidate]      = function (data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_ice_candidate', [info]);
    };

    ProcessCMD[_CMD_.sc_media_incoming_call_missed] = function (data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_incoming_call_missed', [info]);
    };

    ProcessCMD[_CMD_.sc_media_incoming_call_accepted_by_another_cid] = function () {
        $rootScope.$broadcast('sc_media_incoming_call_accepted_by_another_cid');
    };

    // === Broadcasts ===============================================

    ProcessCMD[_CMD_.sc_broadcast]                = function (data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.Broadcast.onReceiveBroadcast, [info]);

        if (!$rootScope.isWebClient){
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_show_on_top,
                true,
                true
            ]);
        }
    };

    ProcessCMD[_CMD_.sc_broadcast_read_notify]    = function (data) {
        var info = mcService.StringToObj(data);

        // "UIN"           : 986,              // идентификатор пользователя, который получил и прочитал оповещение
        // "Nick"          : "Elisa",          // ник робота-отправителя
        // "Avatar"        : 98,               // аватар робота-отправителя
        // "Sex"           : 0,                // пол робота-отправителя
        // "Email"         : "chapay@mail.su", // адрес электронной почты администратора сервера
        // "ID"            : 631,              // идентификатор оповещения
        // "UserNick"      : "Chapay",         // ник пользователя
        // "UserAvatar"    : 98,               // аватар
        // "UserSex"       : 1,                // пол пользователя
        // "ReadedCount"   : 7,                // количество прочитанных оповещений из общего блока отправленного оповещения
        // "TotalCount"    : 10                // (6.3+) общее количество пользователей в оповещении

        $rootScope.$broadcast(window._messages_.chatFrame.addCustomMSG, [{
            UIN: 0, // Elisa
            Msg: mcLang(643, [info.ID, info.ReadedCount, info.TotalCount, info.UserNick]) // "643":"Оповещение №%s (%s/%s) получено: %s",
        }]);

    };

    // ===============================================================

    ProcessCMD[_CMD_.sc_typing_notify]            = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('onTypingNotify', [info]);
    };

    ProcessCMD[_CMD_.sc_get_remote_uin_current_time] = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_send_my_current_time,
            mcConst.SessionID,
            info.UINFrom,
            info.CIDFrom,
            mcService.formatDate(new Date(), 'dd.mm.yyyy.hh.nn.ss')
        ]);
    };

    ProcessCMD[_CMD_.sc_get_uin_info]             = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

        customCallBack(_CMD_.cs_get_uin_info, info);
    };

    // ProcessCMD[_CMD_.sc_channel_history_messages] = function (data){
    //     var info = mcService.StringToObj(data);
    //
    //     $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock
    //
    //     if (!customCallBack(_CMD_.cs_get_channel_history_messages, info)) {
    //         $rootScope.$broadcast('onConfHistory', [info]);
    //     }
    // };

    // ProcessCMD[_CMD_.sc_bbs_changed]              = function (){
    //     $rootScope.$broadcast('hide' + mcConst.lockInterface, []);
    //
    //     $rootScope.$broadcast('receiveBBS');
    // };

    // ProcessCMD[_CMD_.sc_get_contacts_list]        = function (data){
    //     var info = mcService.StringToObj(data);
    //
    //     $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock
    //
    //     customCallBack(_CMD_.cs_get_contacts_list, info);
    // };

    ProcessCMD[_CMD_.sc_update_user_data]         = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

        $rootScope.$broadcast('updateSmallUserInfo', [info]);
    };

    ProcessCMD[_CMD_.sc_sync_private_history]     = function (data){ // cs_sync_private_history_last && cs_sync_private_history
        var info = mcService.StringToObj(data);

        // $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

        if (!(customCallBack(_CMD_.cs_sync_private_history, info) || customCallBack(_CMD_.cs_sync_private_history_last, info))) {
            $rootScope.$broadcast(window._messages_.dialogsList.drawHistoryMessages, [info]);
        }
    };

    // --- files ---

    ProcessCMD[_CMD_.sc_files_success_recieved]       = function (){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_direct_download_complete);
    };
    ProcessCMD[_CMD_.sc_file_internal_sended_ok_idx]  = function (data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_direct_download_file_received, [info]);
    };
    ProcessCMD[_CMD_.sc_files_transfer_accept]        = function (data) {
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_direct_upload_accept, [info]);
    };
    ProcessCMD[_CMD_.sc_files_transfer_progress]      = function (data) {
        // "UIN"             : 54,        // идентификатор отправителя
        // "Percents"        : 67,        // сколько процентов файлов передано
        // "TotalSize"       : 389475683, // общий объём передаваемых файлов
        // "CurrentCount"    : 17,        // сколько файлов уже передано на данный момент
        // "TotalCount"      : 985,       // сколько файлов всего
        // "Speed"           : 654657,    // текущая скорость передачи файлов
        // "CurrentTransfer" : 260948707  // сколько байт на данный момент уже передано

        var info = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.downloadUpload.on_files_transfer_progress, [{
            percent: info.Percents,
            size   : mcService.formatFileSize(info.TotalSize),
            text   : mcService.formatFileSize(info.CurrentTransfer)
        }]);
    };
    ProcessCMD[_CMD_.sc_files_request_transfer]       = function (data) {
        // "UIN"          : 658,                     // идентификатор отправителя
        // "FilesCount"   : 98,                      // количество отправляемых файлов
        // "FilesSize"    : 9854411247,              // общий объём отправляемых файлов, в байтах
        // "DisplayName"  : "Alexey Pikurov",        // (5.16+) имя отправителя
        // "InternalNick" : "Hobit",                 // (5.16+) ник отправителя
        // "Desc"         : "Тестовый набор файлов", // текстовое описание списка файлов
        // "List"         : "......."                // список файлов. Доработать
        var obj = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_request_transfer, [obj]);
        $rootScope.$broadcast(window._messages_.chatFrame.showNotifyMessage, [true, mcLang(600), obj.DisplayName, false, obj.UIN]); // "600":"Запрос на прием файлов!",
    };
    ProcessCMD[_CMD_.sc_file_internal_send_idx]       = function (data) { // началась прямая передача файлов через сервер, еще не реализовано, прерываем передачу с ошибкой
        // "UIN"     : 96,  // идентификатор отправителя
        // "FileIdx" : 27   // номер отправляемого файла из общей структуры файлов
        var obj = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_direct_receive_abort, [obj.UIN]);

        $rootScope.SendCMDToServer([
            mcConst._CMD_.cs_private_msg,
            mcConst.SessionID,

            JSON.stringify({
                UIN : obj.UIN,
                Msg : mcLang(620), // "620":"Невозможно принять файлы из-за фаервола (брандмауэра) или вы находитесь в разных подсетях с вашим собеседником",
                MsgType: mcConst._CMD_.msgType.TEXT
            })
        ]);
    };
    ProcessCMD[_CMD_.ec_file_direct_receive_client_disconnected] = function (data) {
        var obj = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_direct_receive_client_disconnected, [obj.UIN, obj.allOK]);
    };

    ProcessCMD[_CMD_.sc_files_transfer_deny]          =
    ProcessCMD[_CMD_.sc_files_transfer_abort]         =
    ProcessCMD[_CMD_.sc_files_request_timeout]        =
    ProcessCMD[_CMD_.sc_files_transfer_request_abort] = function (data) {
        var uin = mcService.StringToObj(data).UIN;

        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_upload_aborted, [uin]);
    };

    // ==================================================================================

    SendCMDToServer[_CMD_.ns_check_invite] = function ns_check_invite(invite) {
        initSocketIo();

        // mcService.SaveSessionSettings("", "", "", false);

        mcConnect.SetCallBackFunctionByCMD(_CMD_.sn_invite, function () {
            destroySocketIo();

            customCallBack(_CMD_.ns_check_invite, this);
        });

        mcConnect.SetCallBackFunctionByCMD(_CMD_.Blocked, function () {
            var errCode = this;

            console.warn('Blocked');

            $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

            $rootScope.$broadcast('ErrorMsg', [errCode, mcConst.ErrorText[errCode]]);
        });

        mcConnect.SendDataToServer({
            CMD: _CMD_.ns_check_invite,
            Data: invite
        });
    };

    SendCMDToServer[_CMD_.ns_get_by_groups] = function ns_check_invite(goup, online) {
        !online && initSocketIo();

        mcConnect.SetCallBackFunctionByCMD(_CMD_.sn_operators, function () {
            !online && destroySocketIo();

            customCallBack(_CMD_.ns_get_by_groups, this);
        });

        !online && mcConnect.SetCallBackFunctionByCMD(_CMD_.Blocked, function () {
            var errCode = this;

            console.warn('Blocked');

            $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

            $rootScope.$broadcast('ErrorMsg', [errCode, mcConst.ErrorText[errCode]]);
        });

        mcConnect.SendDataToServer({
            CMD: _CMD_.ns_get_by_groups,
            Data: goup
        });
    };

    SendCMDToServer[_CMD_.ns_get_by_uins] = function ns_check_invite(uins, online) {
        !online && initSocketIo();

        mcConnect.SetCallBackFunctionByCMD(_CMD_.sn_operators, function () {
            !online && destroySocketIo();

            customCallBack(_CMD_.ns_get_by_uins, this);
        });

        !online && mcConnect.SetCallBackFunctionByCMD(_CMD_.Blocked, function () {
            var errCode = this;

            console.warn('Blocked');

            $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

            $rootScope.$broadcast('ErrorMsg', [errCode, mcConst.ErrorText[errCode]]);
        });

        mcConnect.SendDataToServer({
            CMD: _CMD_.ns_get_by_uins,
            Data: uins
        });
    };

    SendCMDToServer[_CMD_.Login]             = function CS_CMD_Login(_Auth, _PWD, _servPWD, _RM, _sID) {
        mcConst.LoginInfo.login = _Auth.toString().replace('\\', '/');

        initSocketIo();

        mcConnect.SetCallBackFunctionByCMD(_CMD_.OK, function () {
            var data = this.split(mcConst.CR);

            mcConst.SessionID = data[0];

            //console.log('SessionID: ' + mcConst.SessionID);

            if (data.length > 1) {
                mcConst.LoginInfo.pwd     = data[1];
                mcConst.LoginInfo.servPwd = data[2];

                if (_RM) {
                    mcService.SaveSessionSettings(mcConst.LoginInfo);
                }
            }

            $rootScope.$broadcast('StartPingTimer', []);
        });

        mcConnect.SetCallBackFunctionByCMD(_CMD_.Blocked, function () {
            var errCode = this;

            console.warn('Blocked');

            $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

            mcService.SaveSessionSettings("", "", "", false);

            $rootScope.$broadcast('ErrorMsg', [errCode, mcService.Error(errCode)]);
        });

        var isAdLogin = mcConst.LoginInfo.login.indexOf('/') === -1;

        mcConst.UserInfo.ClientType = _CMD_.ClientType.WEB_SUPPORT;

        mcConnect.SendDataToServer({
            CMD: _CMD_.Login,
            Data: {
                Host    : mcConst.ServerInfo.Host,
                Port    : mcConst.ServerInfo.Port,
                AltHost : mcConst.ServerInfo.AltHost,
                AltPort : mcConst.ServerInfo.AltPort,
                
                Secured : mcService.convertIntToBool(mcConst.ServerInfo.Secured),

                SessionID : _sID,
                
                UIN       : mcConst.LoginInfo.login,
                Pass      : _PWD,
                ServPass  : _servPWD,
                RM        : _RM,
                PingDelay : 15,
                ClientType: mcConst.UserInfo.ClientType, //'web',
                UserAgent : navigator.userAgent || "",
                Referral  : document.referrer || "",
                Style     : isAdLogin ? (_PWD != undefined && _PWD != "" ? mcConst._CMD_.loginStyle.STANDARD : mcConst._CMD_.loginStyle.NO_PASS) : mcConst._CMD_.loginStyle.AD,
                // State     : mcConst.currentState,
                UTC       : (new Date()).getTimezoneOffset() / -60
            }
        });
    };

    SendCMDToServer[_CMD_.Register]          = function Register(_Auth, _PWD, _servPWD, _RM, _Email, _Sex) {
        mcConst.LoginInfo.login = _Auth.toString().replace('\\', '/');

        initSocketIo();

        mcConst.LoginInfo.pwd     = _PWD;
        mcConst.LoginInfo.servPwd = _servPWD;

        // console.log(mcConst.LoginInfo);

        mcConnect.SetCallBackFunctionByCMD(_CMD_.OK, function () {
            var data = this.split(mcConst.CR);

            mcConst.SessionID = data[0];

            //console.log('SessionID: ' + mcConst.SessionID);

            if ($rootScope.isWebClient){
                $rootScope.$broadcast('StartPingTimer', []);
            }
        });

        mcConnect.SetCallBackFunctionByCMD(_CMD_.Blocked, function () {
            var errCode = this;

            $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

            mcService.SaveSessionSettings("", "", "", false);

            $rootScope.$broadcast('ErrorMsg', [errCode, mcService.Error(errCode)]);
        });

        mcConst.UserInfo.ClientType = _CMD_.ClientType.WEB_SUPPORT;

        mcConnect.SendDataToServer({
            CMD: _CMD_.Register,
            Data: {
                Host    : mcConst.ServerInfo.Host,
                Port    : mcConst.ServerInfo.Port,
                AltHost : mcConst.ServerInfo.AltHost,
                AltPort : mcConst.ServerInfo.AltPort,

                Secured : mcService.convertIntToBool(mcConst.ServerInfo.Secured),

                UIN       : mcConst.LoginInfo.login,
                Pass      : _PWD,
                ServPass  : _servPWD,
                Gender    : _Sex,
                Email     : _Email,
                RM        : _RM,
                ClientType: mcConst.UserInfo.ClientType,
                PingDelay : 15,
                UserAgent : navigator.userAgent,
                Referral  : document.referrer,
                Style     : mcConst._CMD_.loginStyle.STANDARD, //mcConst.LoginInfo.login.indexOf('/') == -1 ? 0 : 1,
                // State     : mcConst.currentState,
                UTC       : (new Date()).getTimezoneOffset() / -60
            }
        });
    };

    SendCMDToServer[_CMD_.cs_login_by_token] = function cs_login_by_token(token) {
        mcConst.socketIO = io({
            'reconnection': true,
            'reconnectionAttempts': 2,
            'timeout' : 2000
        });

        mcConst.initWS(mcConst.socketIO);

        mcConnect.SetCallBackFunctionByCMD(_CMD_.OK, function () {
            var data = this.split(mcConst.CR);

            mcConst.SessionID = data[0];

            $rootScope.$broadcast('StartPingTimer', []);
        });

        mcConnect.SetCallBackFunctionByCMD(_CMD_.Blocked, function () {
            var errCode = this;

            mcService.SaveSessionSettings("", "", "", false);

            $rootScope.$broadcast('enable' + mcConst.dataModels.Login, []);
            $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

            $rootScope.$broadcast('ErrorMsg', [errCode, mcService.Error(errCode)]);
        });

        mcConst.UserInfo.ClientType = _CMD_.ClientType.WEB_SUPPORT;

        mcConnect.SendDataToServer({
            CMD : _CMD_.cs_login_by_token,
            Data: {
                Token     : token,
                ClientType: mcConst.UserInfo.ClientType,
                PingDelay : 1,
                UserAgent : navigator.userAgent || "",
                Referral  : document.referrer || "",
                // State     : mcConst.currentState,
                UTC       : (new Date()).getTimezoneOffset() / -60
            }
        });
    };

    SendCMDToServer[_CMD_.Ping]              = function CS_CMD_Ping(sID){
        mcConnect.SendDataToServer({
            CMD  : _CMD_.Ping,
            Data : sID
        })
    };

    SendCMDToServer[_CMD_.cs_quit]           = function cs_quit(sID){
        mcConnect.SendDataToServer({
            CMD  : _CMD_.cs_quit,
            Data : sID,
            immediately: true
        })
    };

    // =====================================================================================

    var sendPing = SendCMDToServer[_CMD_.Ping];
}
/**
 * Created by Gifer on 21.07.2015.
 */

var _Dragging = function(){
    return {
        move : function(divid, xpos, ypos){
            divid.style.left = xpos + 'px';
            divid.style.top  = ypos + 'px';
        },
        startMoving : function(divid, container, evt){
            evt = evt || window.event;

            var posX  = evt.clientX,
                posY  = evt.clientY,
                diffX = posX - divid.offsetLeft,
                diffY = posY - divid.offsetTop,

                parent = document.getElementById(container),

                eWi = parseInt(divid.offsetWidth),
                eHe = parseInt(divid.offsetHeight),
                cWi = parseInt(parent.offsetWidth),
                cHe = parseInt(parent.offsetHeight);

            document.onmousemove = function(evt){
                evt = evt || window.event;

                var posX = evt.clientX,
                    posY = evt.clientY,
                    aX = posX - diffX,
                    aY = posY - diffY;

                if (aX < 0) aX = 0;
                if (aY < 0) aY = 0;
                if (aX + eWi > cWi) aX = cWi - eWi;
                if (aY + eHe > cHe) aY = cHe -eHe;

                _Dragging.move(divid,aX,aY);
            }
        },
        stopMoving : function(){
            document.onmousemove = function(){}
        }
    }
}();

// =================================================================

function setLocalizations(){
    webix.i18n.parseFormat = '%d.%m.%Y';
    webix.i18n.locales["en"] = webix.i18n.locales["en-US"];

    webix.i18n.locales["ru"]={
        groupDelimiter:",",
        groupSize:3,
        decimalDelimeter:".",
        decimalSize:2,

        dateFormat:"%d.%m.%Y",
        timeFormat:"%h:%i %A",
        longDateFormat:"%d %F %Y",
        fullDateFormat:"%d/%m/%Y %h:%i %A",

        price:"${obj}",
        priceSettings:null, //use number defaults


        fileSize: ["b","Kb","Mb","Gb","Tb","Pb","Eb"],

        calendar: {
            monthFull  : ["Январь", "Февраль", "Март", "Апрель", "Май", "Июнь", "Июль", "Август", "Сентябрь", "Октябрь", "Ноябрь", "Декабрь" ],
            monthShort : ["Янв", "Фев", "Мар", "Апр", "Май", "Июн", "Июл", "Авг", "Сен", "Окт", "Ноя", "Дек" ],
            dayFull    : ["Воскресенье", "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота"],
            dayShort   : ["Вс", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"],
            hours      : "Час",
            minutes    : "Минут",
            done       : "Готово"
        },
        controls:{
            select:"Выбрать"
        }
    };

    webix.i18n.locales["uk"]={
        groupDelimiter:",",
        groupSize:3,
        decimalDelimeter:".",
        decimalSize:2,

        dateFormat:"%d.%m.%Y",
        timeFormat:"%h:%i %A",
        longDateFormat:"%d %F %Y",
        fullDateFormat:"%d/%m/%Y %h:%i %A",

        price:"${obj}",
        priceSettings:null, //use number defaults


        fileSize: ["b","Kb","Mb","Gb","Tb","Pb","Eb"],

        calendar: {
            monthFull  : ["Січень", "Лютий", "Березень", "Квітень", "Травень", "Червень", "Липень", "Серпень", "Вересень", "Жовтень", "Листопад", "Грудень" ],
            monthShort : ["Січ", "Лют", "Бер", "Кві", "Тра", "Чер", "Лип", "Сер", "Вер", "Жов", "Лис", "Гру" ],
            dayFull    : ["Неділя", "Понеділок", "Вівторок", "Середа", "Четвер", "П'ятниця", "Субота"],
            dayShort   : ["Нд", "Пн", "Вт", "Ср", "Чт", "Пт", "Сб"],
            hours      : "Годинн",
            minutes    : "Хвилин",
            done       : "Готово"
        },
        controls:{
            select:"Обрати"
        }
    };
}

Date.prototype.startOfDay = function(){
    var now = this.getTime();

    return  now - (this.getHours()*1000*60*60 + this.getMinutes()*1000*60 + this.getSeconds()*1000 + this.getMilliseconds());
};


/**
 * Created by Gifer on 22.08.2017.
 */

function MCPrivatesList($rootScope, _drawMessage, /*_historyList,*/ __statuses, mcUsersSmallInfo) {
    "use strict";

    var __list      = {};
    var historyMsg  = {};
    var megaIdx     = 999999999;
    var DlgIdx      = 0;
    var backupDlgId = 0;
    var userFields  = $rootScope.isWebClient ? mcConst.defaultUserFields : mcConst.ClientSettings.AdditionalPrivateInfoFields;
    var saveTimeout = null;

    __statuses.onStatesChange("privates", function (states) {
        Object.keys(__list).forEach(function (uin) {
            __setState(uin, states[uin]);
        });

        // __notifyOfflineMessages();
    });

    function __getPrivate(uin, noCopy) {
        var res = null;

        if (__list.hasOwnProperty(uin)){
            res = noCopy ? __list[uin] : mcService.Marge({}, __list[uin]);
        } else {
            // console.warn("getPrivate: Privates list hasn't a private with UIN: " + uin);
        }

        return res;
    }

    function __updatePrivate(uin, data) {
        __hasPrivate(uin) && mcService.Marge(__getPrivate(uin, true), data);
    }

    function __needHistory(uin) {
        if (__list.hasOwnProperty(uin)){
            return __list[uin].needHistory;
        }
    }

    function __noHistory(uin) {
        if (__list.hasOwnProperty(uin)){
            __list[uin].needHistory = false;
        }
    }

    function __hasMessage(uin, idx) {
        var res  = __getPrivate(uin, true);

        if (res){
            res = res.hasMsgIdx(idx);
        }

        return res;
    }

    function __setLastSpeaker(uin, speaker) {
        if (__hasPrivate(uin)){
            __getPrivate(uin, true).lastSpeaker = speaker;
        }
    }

    function __getLastSpeaker(uin) {
        if (__hasPrivate(uin)){
            return __getPrivate(uin).lastSpeaker;
        }
    }

    function __hasPrivate(uin) {
        return __list.hasOwnProperty(uin);
    }

    function __setIsPrivateOpen(uin) {
        if (__hasPrivate(uin)) __getPrivate(uin, true).isOpen = true;
    }

    function __serializeDialogs() {
        var res = {
            DlgIdx : DlgIdx,
            Data   : []
        };

        __eachUser(function (uin, user) {
            res.Data.push(user);
        });

        return res;
    }

    function __saveDialogsToStorage(_DlgIdx){
        DlgIdx = _DlgIdx ? _DlgIdx : DlgIdx + 1;
        
        mcConst.UserInfo.PvDlgIdx = DlgIdx; // меняем ID диалогов

        // if (!saveTimeout){
        //     saveTimeout = setTimeout(function () {
        //         $rootScope.Storage.dialogs.save(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.DIALOG_LIST, JSON.stringify(__serializeDialogs()));
        //
        //         saveTimeout = null;
        //     }, 500);
        // }
    }

    function __closePrivate(uin, user) {
        if (__hasPrivate(uin)) user.isOpen = false;
    }

    function __isOpen(uin) {
        return __hasPrivate(uin) ? __getPrivate(uin, true).isOpen : false;
    }

    function __eachUser(func) {
        Object.keys(__list).forEach(function (uin) {
            func(uin, __list[uin]);
        });
    }

    function __getTopInListMsgIdx(uin) {
        return __hasPrivate(uin) ? __list[uin].firstMsgIdx : megaIdx;
    }

    function __getBottomInListMsgIdx(uin) {
        return __hasPrivate(uin) ? __list[uin].IDMsgLast : -1;
    }

    function __getLastGotMsgIdx(uin) {
        return __hasPrivate(uin) ? __list[uin].IDMsgGot : 0;
    }

    function __getUpSyncCount(uin) {
        return __hasPrivate(uin) ? __list[uin].upSync : 0;
    }

    function __getLastReadMsgIdx(uin) {
        return __hasPrivate(uin) ? __list[uin].IDMsgRead : -1;
    }

    function __getMsgIdx(uin, idx) {
        return !__list.hasOwnProperty(uin) || !__list[uin].hasMsgIdx(idx);
    }

    function __getMaxMessageListIdx(priv){
        var res = 0;

        if (priv && priv.messagesList) {
            priv.messagesList.forEach(function (msgId){
                if (msgId > res){
                    res = msgId;
                }
            });
        }

        return res;
    }

    // === Private History ===================

    function __drawHistoryMessages(inData, noScrollDown){
        //                                                       //     "UINWith" : 17, // (5.24+) пользователь, с которым был приватный разговор
        // [{                                                    //     "Data"    :[{
        //     Idx: 57435                                        //         "UIN"     : 98,                   // кто отправил сообщение
        //     Msg: "L$люди реально есть: *6-220915110051.jpg" //         "dtUTC"   : "2015.09.01.17.18.35" // когда (UTC)
        //     MsgType: 1                                        //         "MsgType" : 1,                    // тип сообщения
        //     State: 3                                          //         "State"   : 3,                    // статус сообщения
        //     UIN: 6                                            //         "Idx"     : 75,                   // уникальный индекс разговора (пары uin1/uin2)
        //     dt: "22.09.2015.12.10.26"                         //         "Msg"     : "Hello!"              // тело сообщения
        // }]                                                    //     }]
        var source   = inData.Data;
        var uin      = inData.UINWith;
        var needMore = 0;

        $rootScope.$broadcast("syncHistoryDone", ["UIN-" + uin]);

        if (source && source.length){
            for ( var i = source.length - 1; i >= 0; i-- ) {
                var UINFrom = source[i].UIN;

                if (source[i].MsgType === mcConst._CMD_.msgType.ADM_DELETE_MESSAGE || source[i].Mod === mcConst._CMD_.msgMods.DELETED){
                    needMore++;
                }

                if (source[i].Mod !== mcConst._CMD_.msgMods.DELETED && __getMsgIdx(UINFrom, source[i].Idx)){
                    var user    = source[i].UIN == mcConst.UserInfo.UIN ? mcConst.UserInfo : __getPrivate(source[i].UIN, true);
                    var UINTo   = source[i].UIN == mcConst.UserInfo.UIN ? uin              : mcConst.UserInfo.UIN;

                    if (user){
                        var out     = {
                            UINFrom   : UINFrom,
                            UINTo     : UINTo,
                            Nick      : user.Nick || user.DisplayName,
                            StateFrom : user.State,
                            Sex       : user.Sex,
                            Mod       : source[i].Mod,
                            Idx       : source[i].Idx,
                            Msg       : source[i].Msg,
                            dtUTC     : source[i].dtUTC || source[i].dt,
                            MsgType   : source[i].MsgType,
                            noScrollDown: noScrollDown,
                            noNotify  : true,
                            History   : true  
                        };

                        __privateMessage(out);
                    } else {
                        console.warn("User UIN: " + source[i].UIN + " closed. History skipped.");

                        break;
                    }
                }
            }
        }

        return needMore;
    }

    function __drawUpSyncHistoryMessages(inData, more, noUpdateCounter){
        //     "UINWith" : 17,                       // (5.24+) пользователь, с которым был приватный разговор
        //     "Data"    :[{
        //         "UIN"     : 98,                   // кто отправил сообщение
        //         "dtUTC"   : "2015.09.01.17.18.35" // когда (UTC)
        //         "MsgType" : 1,                    // тип сообщения
        //         "State"   : 3,                    // статус сообщения
        //         "Idx"     : 75,                   // уникальный индекс разговора (пары uin1/uin2)
        //         "Msg"     : "Hello!"              // тело сообщения
        //     }]
        
        var source   = inData.Data;
        var uin      = inData.UINWith;
        var needMore = 0;

        $rootScope.$broadcast("syncHistoryDone", ["UIN-" + uin]);

        if (source && source.length){
            for ( var i = 0; i <= source.length - 1; i++ ) {
                var UINFrom = source[i].UIN;

                if (source[i].MsgType === mcConst._CMD_.msgType.ADM_DELETE_MESSAGE || source[i].Mod === mcConst._CMD_.msgMods.DELETED){
                    needMore ++;
                }

                if (source[i].Mod !== mcConst._CMD_.msgMods.DELETED && __getMsgIdx(UINFrom, source[i].Idx)){
                    var user     = source[i].UIN == mcConst.UserInfo.UIN ? mcConst.UserInfo    : __getPrivate(source[i].UIN, true);
                    var UINTo    = source[i].UIN == mcConst.UserInfo.UIN ? uin                 : mcConst.UserInfo.UIN;
                    var lastRead = source[i].UIN != mcConst.UserInfo.UIN ? __getMyMsgRead(uin) : false;

                    // console.log("lastRead: " + lastRead);
                    // console.log("source[i].Idx: " + source[i].Idx);

                    if (user){
                        var out     = {
                            UINFrom   : UINFrom,
                            UINTo     : UINTo,
                            Nick      : user.Nick || user.DisplayName,
                            StateFrom : user.State,
                            Sex       : user.Sex,
                            Mod       : source[i].Mod,
                            Idx       : source[i].Idx,
                            Msg       : source[i].Msg,
                            dtUTC     : source[i].dtUTC || source[i].dt,
                            MsgType   : source[i].MsgType,
                            // noNotify  : lastRead && source[i].Idx <= lastRead, // false,
                            noNotify  : false,
                            History     : more,
                            noScrollDown: more,
                            // UpSync      : lastRead && source[i].Idx <= lastRead,
                            UpSync      : false,
                            noUpdateCounter: noUpdateCounter
                        };

                        __privateMessage(out);
                    } else {
                        console.warn("User UIN: " + source[i].UIN + " closed. History skipped.");

                        break;
                    }
                }
            }
        }

        return needMore;
    }

    function __getPrivateHistory(uin, count){
        if ($rootScope.isWebClient || (mcConst.ClientSettings.SysEventsLoadHistoryType && mcConst.ClientSettings.LoadHistoryToPrivate)){
            __noHistory(uin);

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_sync_private_history_last,
                mcConst.SessionID,
                uin,
                count || mcConst.countHistoryMessagesLoad,

                function (data) {
                    var deleted = __drawHistoryMessages(data);

                    if ((deleted > (count || mcConst.countHistoryMessagesLoad)/3) && data.Data.length !== 0){
                        __syncPrivateHistory(uin, false);
                    }
                }
            ]);
        }
    }

    function __syncPrivateHistory(uin, noScrollDown) {
        var range = __getTopInListMsgIdx(uin);
        var loadCount = 0;

        if (range > 1 && __hasPrivate(uin)){
            if (range > mcConst.countHistoryMessagesLoad) {
                range = (range - mcConst.countHistoryMessagesLoad) + "-" + range;
                loadCount = range - (range - mcConst.countHistoryMessagesLoad);
            } else {
                range = "1-" + range;
                loadCount = + range - 1;
            }

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_sync_private_history,
                mcConst.SessionID,

                uin,
                range,

                function (data){
                    var deleted = __drawHistoryMessages(data, noScrollDown);

                    if ((deleted > loadCount/3) && data.Data.length !== 0){
                        __syncPrivateHistory(uin, noScrollDown);
                    }
                }
            ]);
        } else {
            $rootScope.$broadcast("syncHistoryDone", ["UIN-" + uin]);
        }
    }

    function __upSyncPrivateHistory(uin) { // получить месаджи, которые были отправлены пока пользователь был не в сети
        var range = __getBottomInListMsgIdx(uin);

        if (range.toString() !== '-1' && __hasPrivate(uin)){
            var got    = __getLastGotMsgIdx(uin);
            var upSync = __getUpSyncCount(uin);

            if (upSync){
                range = upSync;
            } else
            if (range === got){
                return; //
            } else
            if (range > got) {
                range = (got + 1) + "-" + range;
            } else {
                range = "1-" + range;
            }

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_sync_private_history,
                mcConst.SessionID,

                uin,
                range,

                function (data){
                    __drawUpSyncHistoryMessages(data, false, true);
                }
            ]);
        }
    }

    // === My Messages ====

    function __addPrivateMessage(uin, Idx, read) {
        var res = false;

        if (__hasPrivate(uin)){
            var user = __getPrivate(uin, true);

            if (user.messagesList.indexOf(Idx) === -1) {
                user.messagesList.push(Idx);
            }

            if (user.IDMsgLast <= Idx){ // новый месадж || (user.IDMsgLast === -1 && Idx !== -1)) {
                user.IDMsgLast = Idx;

                if (user.IDMsgGot <= Idx) {
                    user.IDMsgGot = Idx;

                    if (read){
                        user.IDMsgRead = Idx;
                    }

                    res = true;
                }
            }

            if (user.firstMsgIdx >= Idx || (user.firstMsgIdx === megaIdx && Idx !== -1)) {
                user.firstMsgIdx = Idx;
            }
        }

        return res;
    }

    function __setMyMsgReadState(uin, state) {
        if (__hasPrivate(uin)){
            var user = __getPrivate(uin, true);

            user.myMsg = mcService.Marge(user.myMsg, state);
        }
    }

    function __setMyMsgRead(uin, idx) {
        if (__hasPrivate(uin)){
            __getPrivate(uin, true).myMsg.IDMyMsgRead = idx;
            __getPrivate(uin, true).myMsg.IDMyMsgGot  = idx;
        }
    }

    function __setMyMsgGot(uin, idx) {
        if (__hasPrivate(uin)){
            __getPrivate(uin, true).myMsg.IDMyMsgGot = idx;
        }
    }

    function __getMyMsgReadState(uin) {
        if (__hasPrivate(uin)){
            return __getPrivate(uin, true).myMsg;
        }
    }

    function __getMyMsgRead(uin) {
        return __hasPrivate(uin) ? __getPrivate(uin, true).myMsg.IDMyMsgRead : 0;
    }

    function __getMyMsgGot(uin) {
        return __hasPrivate(uin) ? __getPrivate(uin, true).myMsg.IDMyMsgGot : 0;
    }

    function __getHistoryMsg(_uin, _idx){
        var uin = _uin.toString();
        var idx = _idx.toString();
        
        return historyMsg[uin]
            ? historyMsg[uin][idx] ? historyMsg[uin][idx] : null
            : null;
    }

    function __privateMessage(msg){
        function _drawMsg(msg) {
            var _priv = __getPrivate(msg.UINFrom != mcConst.UserInfo.UIN ? msg.UINFrom : msg.UINTo);

            if (_priv){
                _drawMessage(msg);

                // if (!msg.History){
                //     _historyList.addOrUpdateUser(_priv, msg.dt);
                // }
            }
        }

        if (msg.hasOwnProperty('UIN') && !msg.hasOwnProperty('UINFrom')){
            msg.UINFrom = msg.UIN;

            delete msg.UIN;
        }

        msg.UINTo   = msg.UINTo.toString();
        msg.UINFrom = msg.UINFrom.toString();
        msg.dt      = mcService.utcTime(msg);
        msg.Idx     = msg.Idx || msg.MsgIdx;

        var msgCut = msg.MsgType === mcConst._CMD_.msgType.REPLY
            ? mcService.copy(mcService.StringToObj(msg.Msg).Reply.Msg, 0, 50)
            : mcService.copy(msg.Msg, 0, 50);


        if (msg.hasOwnProperty('UINFrom')) {
            if (msg.UINFrom != mcConst.UserInfo.UIN && !__hasPrivate(msg.UINFrom)) { // говорю не я или пришел мессадж от юзера, с которым у меня не открыт приват
                var user = {
                    IDMsgRead   : msg.Idx - 1,
                    IDMsgGot    : msg.Idx,
                    IDMyMsgRead : msg.Idx - 1,
                    IDMyMsgGot  : msg.Idx,
                    IDMsgLast   : msg.Idx,
                    ChangeID    : msg.ChangeID,
                    UIN         : msg.UINFrom,
                    MsgCut      : msgCut,
                    Speaker     : msg.UINFrom,
                    State       : msg.StateFrom,
                    Nick        : msg.Nick,
                    dt          : msg.dtUTC
                };

                if (msg.DlgIdx) {
                    DlgIdx = msg.DlgIdx;
                }

                if (!historyMsg[msg.UINFrom]){
                    historyMsg[msg.UINFrom] = {};
                }

                historyMsg[msg.UINFrom][msg.Idx] = JSON.stringify(mcService.Marge({dtUTC: mcService.utcTime(msg, true), UIN: msg.UINFrom}, msg)); // сохраняем месадж для реплая

                $rootScope.$broadcast(window._messages_.usersSmallInfo.getInfoByUserUINs, [user.UIN, function (info) {
                    user = mcService.Marge(user, info);

                    __addPrivate(user);

                    msg.Nick = user.DisplayName;

                    // if (mcService.inArray(msg.MsgType, [
                    //     mcConst._CMD_.msgType.ADM_DELETE_MESSAGE,
                    //     mcConst._CMD_.msgType.PRIVATE_REDIRECT,
                    //     mcConst._CMD_.msgType.BEEP]) === -1
                    // ) {
                    //     $rootScope.$broadcast('updateCounter', [mcService.Marge({
                    //         MsgCut : msgCut,
                    //         Speaker: msg.UINFrom,
                    //         State  : msg.StateFrom,
                    //         Nick   : msg.Nick
                    //     }, msg), true]);
                    // }

                    _drawMsg(msg);
                }]);
            } else { // уже существующий приват в диалогах
                var _uin   = msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom;

                if (!historyMsg[_uin]){
                    historyMsg[_uin] = {};
                }

                historyMsg[_uin][msg.Idx] = JSON.stringify(mcService.Marge({dtUTC: mcService.utcTime(msg, true), UIN: msg.UINFrom}, msg)); // сохраняем месадж для реплая

                if (__hasPrivate(_uin) && !msg.History && !msg.UpSync && !msg.noUpdateCounter
                    // && mcService.inArray(msg.MsgType, [
                    //     mcConst._CMD_.msgType.ADM_DELETE_MESSAGE,
                    //     mcConst._CMD_.msgType.PRIVATE_REDIRECT,
                    //     mcConst._CMD_.msgType.BEEP]) === -1
                ) {
                    // $rootScope.$broadcast('updateCounter', [mcService.Marge({
                    //     MsgCut  : msgCut,
                    //     Speaker : msg.UINFrom,
                    //     State   : msg.UINFrom == mcConst.UserInfo.UIN ? msg.StateTo : msg.StateFrom,
                    //     Nick    : __getPrivateNick(_uin)
                    // }, msg), msg.upSync]);

                    __updatePrivate(_uin, {
                        MsgCut  : msgCut,
                        Speaker : msg.UINFrom,
                        State   : msg.UINFrom == mcConst.UserInfo.UIN ? msg.StateTo : msg.StateFrom,
                        Nick    : __getPrivateNick(_uin),
                        dt      : msg.dtUTC
                    });
                }

                _drawMsg(mcService.Marge(msg, {
                    Nick: msg.UINFrom == mcConst.UserInfo.UIN ? mcConst.UserInfo.Nick :__getPrivateNick(msg.UINFrom)
                }));
            }

            __setLastSpeaker(msg.UINFrom != mcConst.UserInfo.UIN ? msg.UINFrom : msg.UINTo, msg.UINFrom);
        } else {
            console.error('No user info in message: ');
            console.log(msg);
        }

        if (!msg.History && !msg.UpSync){
            __saveDialogsToStorage(DlgIdx);
        }
    }
    
    function __setAllMsgRead(uin) {
        var lastID = __getBottomInListMsgIdx(uin);

        if (__hasPrivate(uin) && lastID > 0 && lastID !== __getPrivate(uin, true).IDMsgRead && __getLastSpeaker(uin) != mcConst.UserInfo.UIN) {
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_private_msg_read,
                mcConst.SessionID,

                uin,
                __getLastGotMsgIdx(uin)
            ]);
        }
    }

    // === Profile ========

    function __getProfileInformation(uin, changeID, cb) {
        var privat = __getPrivate(uin, true);

        if (privat){
            if (privat.ChangeID != changeID || !privat.Information.DISPLAYNAME){
                var Q = new mcService.TaskList();
                var profile = {};

                // update private info if needed
                Q.AddTask(function () {
                    if (!profile.change_id || profile.change_id != changeID) {
                        $rootScope.$broadcast('SendCMDToServer' , [
                            mcConst._CMD_.cs_get_private_info,
                            mcConst.SessionID,

                            uin,
                            userFields,

                            function( data ){
                                privat.Information = data;

                                Q.Next();
                            }
                        ]);
                    } else {
                        privat.Information = profile.profile;

                        Q.Next();
                    }
                });

                // save updated info to storage
                Q.AddTask(function () {
                    if (!$rootScope.isWebClient){
                        $rootScope.$broadcast('sendCMDToElectron', [
                            mcConst._CMD_.ce_profile_save,

                            $rootScope.UNIT.ctrlServersManager.getServerIdByServID(mcConst.ClientSettings.ServID),
                            uin,
                            changeID || privat.ChangeID,
                            privat.Information,

                            function(){
                                Q.Next();
                            }
                        ]);
                    } else {
                        Q.Next();
                    }
                });

                // get user photo and update "usersSmallInfo"
                Q.AddTask(function () {
                    $rootScope.$broadcast('SendCMDToServer', [
                        mcConst._CMD_.cs_get_uin_foto,
                        mcConst.SessionID,

                        uin,

                        function (photo) {
                            privat.Information.Photo = photo;

                            if (cb) {
                                cb(mcService.Marge({}, privat.Information));
                            }
                        }
                    ]);
                });

                Q.Run();
            } else

            // return info if no changes
            if (privat.ChangeID == changeID){
                if (cb) {
                    cb(mcService.Marge({}, privat.Information));
                }
            }
        }
    }

    function __getUserPhoto(uin) {
        var privat = __getPrivate(uin, true);
        
        if (privat && privat.Information){
            return privat.Information.Photo;
        } else {
            return null;
        }
    }

    function __getActiveProcess(uin) {
        // get user active process caption
        // todo: separate to new function
        if (mcService.checkRights(mcConst._CMD_.RS.QAllowViewRemoteUserActiveProcess)) {
            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_get_active_window_caption,
                mcConst.SessionID,

                uin,

                function (info) {
                    $rootScope.$broadcast(window._messages_.privateInfo.updateUserActiveProcess, [info.UINFrom, info]);
                }
            ]);
        }
    }

    // ====================

    function __getPrivateNick(uin) {
        return __hasPrivate(uin) ? __list[uin].DisplayName : "_NoName_" + uin;
    }

    function __showPrivate(uin) {
        var privat = __getPrivate(uin, true);

        if (privat && $rootScope.GetChatTypeID() !== $rootScope.createUIN(uin) && !__getMyMsgGot()){
            __setIsPrivateOpen(uin);

            // get messages state
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_private_get_dialog_msg_states,
                mcConst.SessionID,

                uin,
                function (data) {
                    __setMyMsgReadState(data.UIN, {
                        UIN        : data.UIN,
                        IDMyMsgGot : data.IDMsgGot,
                        IDMyMsgRead: data.IDMsgRead
                    });

                    $rootScope.$broadcast("updateMessagesState", [data.UIN]);
                }
            ]);

            $rootScope.SetPrivate(uin);

            if (!$rootScope.isWebClient){
                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_hide_notify
                ]);
            }
        }

        if (privat){
            $rootScope.needFoto = !!__list[uin].Avatar;

            // $rootScope.$broadcast('selectTool',          [mcConst.dataModels.PrivateInfo]);
            $rootScope.$broadcast('fillChatFrame',       [__getPrivateNick(uin), "", (__getTopInListMsgIdx(uin) !== megaIdx) || __needHistory(uin)]);
            // $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.ChatFrame]);

            if (__needHistory(uin)) {
                __getPrivateHistory(uin);
            }
        }

        var lastID = __getBottomInListMsgIdx(uin);
        var readID = __getPrivate(uin, true).IDMsgRead;

        if (lastID > 0 && lastID !== readID && mcConst.isWindowFocused){
            __getPrivate(uin, true).IDMsgRead = lastID;

            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_private_msg_read,
                mcConst.SessionID,

                uin,
                lastID
            ]);
        }
    }

    function __addPrivate(userPrivate) {
        var res = false;

        if (userPrivate && userPrivate.UIN != mcConst.UserInfo.UIN){
            if (__hasPrivate(userPrivate.UIN)){
                userPrivate = mcService.Marge(__list[userPrivate.UIN], userPrivate);
            } else {
                res = true;
                // DlgIdx
                userPrivate.needHistory  = true;
                userPrivate.isOpen       = false;
                userPrivate.messagesList = [];
                userPrivate.firstMsgIdx  = megaIdx;

                userPrivate.ChangeID     = userPrivate.ChangeID !== void(0) ? userPrivate.ChangeID : -1;
                userPrivate.Information  = {};

                userPrivate.IDMsgGot     = userPrivate.IDMsgGot  || 0;
                userPrivate.IDMsgLast    = userPrivate.IDMsgLast == 0 && userPrivate.IDMsgGot > 0 ? userPrivate.IDMsgGot : userPrivate.IDMsgLast;
                userPrivate.IDMsgRead    = userPrivate.IDMsgRead || 0;
                userPrivate.lastSpeaker  = 0;
                userPrivate.hasMsgIdx    = function (idx) {
                    return userPrivate.messagesList.indexOf(idx) !== -1;
                };
            }

            userPrivate.DisplayName = userPrivate.DisplayName || userPrivate.Nick;
            userPrivate.myMsg       = {
                IDMyMsgRead : userPrivate.IDMyMsgRead || 0,
                IDMyMsgGot  : userPrivate.IDMyMsgGot  || 0
            };

            __list[userPrivate.UIN] = userPrivate;

            if (userPrivate.hasOwnProperty('State')){
                __statuses.setUserState(userPrivate.UIN, userPrivate.State);
            }

            $rootScope.$broadcast(window._messages_.usersSmallInfo.setUserSmallInfo, [{
                UIN         : userPrivate.UIN,
                DisplayName : userPrivate.DisplayName,
                ChangeID    : userPrivate.ChangeID,
                Avatar      : userPrivate.Avatar,
                Sex         : userPrivate.Sex
            }]);

            // $rootScope.$broadcast(window._messages_.dialogsCtrl.addOrUpdatePrivateDialog, [__getPrivate(userPrivate.UIN)]);
        }

        return res;
    }

    function __removePrivate(uin) {
        if (__hasPrivate(uin)){
            delete __list[uin];

            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_del_private_dialog,
                mcConst.SessionID,

                uin
            ]);

            return true;
        }
    }

    function __setState(uin, state) {
        if (__hasPrivate(uin)){
            __getPrivate(uin, true).State = __statuses.setUserState(uin, state);

            $rootScope.$broadcast('changeStateForUser', [uin, state]);

            return true;
        }
    }

    function __clearPrivates() {
        __list = {};
    }
    
    function __closeAllPrivates() {
        __eachUser(__closePrivate);
    }

    function __clearHistory(uin, leave) {
        if (__hasPrivate(uin)) {
            var priv = __getPrivate(uin, true);

            priv.messagesList = priv.messagesList.sort(function(a,b){return mcService.sortAB(b,a)});

            var delList = [];

            if (leave) {
                delList           = mcService.copy(priv.messagesList, leave);
                priv.messagesList = mcService.copy(priv.messagesList, 0, leave - 1);
                priv.firstMsgIdx  = priv.messagesList[priv.messagesList.length - 1];
            } else {
                delList           = priv.messagesList;
                priv.messagesList = [];
                priv.firstMsgIdx  = megaIdx;
            }

            $rootScope.$broadcast(window._messages_.chatFrame.clearPrivateMessages, [uin, delList]);
        }
    }

    // === Dialogs ==============================

    function __getDialogsList(cb) {
        var userList    = null;
        var task        = new mcService.TaskList();
        var isReconnect = !mcService.isObjectEmpty(__list);
        var noChanges   = false;

        // task.AddTask(function () {
        //     $rootScope.Storage.dialogs.load(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.DIALOG_LIST, function (data) {
        //         userList = mcService.StringToObj(data);
        //
        //         task.Next();
        //     });
        // });
                                                                        
        // Get private dialogs list
        /*task.AddTask(function () {
            if (userList && userList.DlgIdx && userList.DlgIdx === DlgIdx) {
                noChanges = !!userList.Data[0].DisplayName; // если нет имени у юзера, то в хранилище список без имен - нужно обновить

                // console.log('noChanges');

                __eachUser(function (uin, user) {
                    user.isOpen = false;
                });

                task.Next();
            } else {
                $rootScope.$broadcast('SendCMDToServer', [
                    mcConst._CMD_.cs_get_private_dialogs,
                    mcConst.SessionID,

                    function (data) {
                        userList = data;

                        task.Next();
                    }
                ]);
            }
        });*/

        // Get privates DisplayName
        /*task.AddTask(function () {
            // if (noChanges) {
            if (true) {
                task.Next();
            } else {
                var needUpdate = [];

                userList.Data.forEach(function (user) {
                    needUpdate.push(user.UIN);
                });

                $rootScope.$broadcast(window._messages_.usersSmallInfo.getInfoByUserUINs, [
                    needUpdate.join(','),

                    function () {
                        userList.Data.forEach(function (user) {
                            var findUser = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [user.UIN]);

                            if (findUser) {
                                user.DisplayName = findUser.DisplayName;
                            } else {
                                console.error("Can't find user on server from dialogs list");
                            }
                        });

                        // сохраняем диалоги в хранилище
                        // $rootScope.Storage.dialogs.save(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.DIALOG_LIST, JSON.stringify(userList), task.Next);
                        task.Next();
                    }
                ]);
            }
        });*/

        // Draw dialogs list
        task.AddTask(function () {
            //"DlgIdx" : 7145, // индекс последнего изменения приватных диалогов
            //"Data"   : [
            //    {
            //        "UIN"         : 17,                    // идентификатор собеседника
            //        "IDMsgLast"   : 4567,                  // ID последнего отправленного сообщения
            //        "IDMsgGot"    : 4560,                  // ID последнего полученного сообщения
            //        "IDMsgRead"   : 4560,                  // ID последнего прочитанного сообщения
            //        "dt"          : "26.11.2012.09.18.31", // дата и время последнего сообщения в UTC в формате dd.mm.yyyy.hh.nn.ss.zzz
            //        "DisplayName" : "John Smith",          // отображаемое имя собеседника в чате
            //        "Avatar"      : 1234245435,            // CRC32 контрольная сумма фотографии пользователя
            //        "Sex"         : 1                      // пол пользователя, 0 - unknown, 1 - male, 2 - female
            //        "Speaker"     : 18,                    // (8.0+) кто сказал сообщение
            //        "MsgType"     : 0,                     // (8.0+) тип сообщения
            //        "MsgCut"      : "bla-bla"              // (8.0+) текст сообщения, первые 64 символа
            //        "ChangeID"    : 123,                   // (8.0+) числовой идентификатор изменений собеседника
            //    },
            var newMsgCount = 0;

            /*__closeAllPrivates();

            if (userList.Data.length){
                userList.Data.forEach(function (user) {
                    if (user) {
                        user.State = mcConst.states.offline;

                        if (isReconnect && DlgIdx && __hasPrivate(user.UIN) && //__isOpen(user.UIN) &&
                            // userList.DlgIdx > DlgIdx &&
                            __getLastGotMsgIdx(user.UIN) < user.IDMsgLast
                            // __getMyMsgRead(user.UIN)     < user.IDMsgRead
                        ) {
                            // console.log("__getLastGotMsgIdx(user.UIN): " + __getLastGotMsgIdx(user.UIN));
                            // console.log("user.IDMsgLast: " + user.IDMsgLast);
                            
                            user.upSync      = (__getLastGotMsgIdx(user.UIN) + 1) + "-" + user.IDMsgLast;
                            // user.upSync      = (__getMaxMessageListIdx() + 1) + "-" + user.IDMsgLast;
                            user.IDMyMsgRead = user.IDMyMsgRead || 0;
                            user.IDMyMsgGot  = user.IDMyMsgGot  || 0;
                        } else {
                            user.upSync = "";
                        }

                        newMsgCount += user.IDMsgLast - user.IDMsgRead;

                        __addPrivate(user);
                    }
                });
            } else {
                //$rootScope.$broadcast(window._messages_.dialogsList.OpenPrivate, ['0']);
            }

            if (newMsgCount) $rootScope.$broadcast(window._messages_.chatFrame.showNotifyMessage, [true, mcLang(713) + newMsgCount, "", false]);*/

            DlgIdx = mcConst.UserInfo.PvDlgIdx;// = userList.DlgIdx;

            __statuses.loadUserStatuses();

            // userList.Data = [];
            // userList      = {};

            task.Next();
        });

        // Уведомляем всех пользователей из диалогов, что я получил их мессаджи (но не прочитал, для этого нужно открыть приват)
        task.AddTask(function () {
            __eachUser(function (uin, user) {
                if (isReconnect && user.upSync) { // если это восстановление связи - дочитываем новые мессаджи от юзеров
                    __upSyncPrivateHistory(uin);
                }

                if (user.IDMsgGot < user.IDMsgLast) {
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_private_msg_got,
                        mcConst.SessionID,

                        user.UIN,
                        user.IDMsgLast
                    ]);

                    user.IDMsgGot = user.IDMsgLast;
                }
            });

            // $rootScope.$broadcast(window._messages_.dialogsCtrl.sortDialogsList);

            cb && cb();
        });

        task.Run();
    }

    function __setDialogIdx(idx) {
        DlgIdx = idx;
    }

    // ==========================================
    // ==========================================

    this.setAllMsgRead = __setAllMsgRead;

    this.showPrivate = __showPrivate;

    // Open on PrivateRequest
    this.addPrivate = function (info) { // onlyAdd используется при вычитке списка приватов с хранилища, поэтому не нужно его дабвлять в историю или сразу показывать в окне
        $rootScope.$broadcast(window._messages_.usersSmallInfo.getInfoByUserUINs, [info.UIN, function () {
            info = mcService.Marge(info, $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [info.UIN]));
            
            if (__addPrivate(info)) {
                // if (info && info.UIN != mcConst.UserInfo.UIN) {
                //     _historyList.addOrUpdateUser(info, mcService.formatDate(new Date(), 'dd.mm.yyyy.hh.nn.ss'));
                // }
            }

            if (__getPrivateNick(info.UIN)) {
                __showPrivate(info.UIN);
            } else {
                $rootScope.$broadcast(window._messages_.usersSmallInfo.getInfoByUserUINs, [info.UIN, function () {
                    info.DisplayName = __getPrivate(info.UIN, true).DisplayName = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [info.UIN]).DisplayName;

                    __showPrivate(info.UIN);
                }
                ]);
            }
        }]);
    };

    this.updateUserInfo = function (uin, newData) {
        if (__hasPrivate(uin)){
            __getProfileInformation(uin, newData.ChangeID, function (information) {
                // $rootScope.$broadcast(window._messages_.privateInfo.setChangeIdForUser, [uin, information]);

                __list[uin] = mcService.Marge(__list[uin], newData);
                __list[uin].Information = information;

                $rootScope.$broadcast(window._messages_.usersSmallInfo.setUserSmallInfo, [{
                    UIN         : newData.UIN,
                    DisplayName : newData.DisplayName,
                    ChangeID    : newData.ChangeID,
                    Avatar      : newData.Avatar,
                    Sex         : newData.Sex
                }]);

                if (newData.State !== void(0)) {
                    __setState(uin, newData.State);
                }

                // mcConst.UserInfo.PvDlgIdx = DlgIdx++;
                __saveDialogsToStorage();
            });
        } else
        if (uin != mcConst.UserInfo.UIN){
            console.warn("updateUserInfo: Can't update user info for UIN: " + uin)
        }
    };

    this.getPrivateHistory = __getPrivateHistory;

    this.hasPrivate = __hasPrivate;

    this.hasMessage = __hasMessage;

    this.addMessage = __addPrivateMessage;

    this.isOpen = __isOpen;

    this.closeAllPrivates = __closeAllPrivates;

    this.getPrivateInfo = function (uin) {
        return __getPrivate(uin);
    };

    this.removePrivate = function (uin) {
        if (__removePrivate(uin)){
            // __savePrivatesToStorage();
        }
    };

    this.setState = __setState;

    this.getFirstIdx = __getTopInListMsgIdx;

    this.getLastIdx = __getBottomInListMsgIdx;

    this.getHistoryMsg = __getHistoryMsg;

    this.getReadIdx = __getLastReadMsgIdx;

    this.setMyMsgGot = __setMyMsgGot;

    this.setMyMsgRead = __setMyMsgRead;

    this.getMyMsgReadState = __getMyMsgReadState;

    this.getDialogsList = __getDialogsList;

    this.statuses = {
        get: __statuses.getUserState,
        set: __setState,
        getAll: __statuses.getAllState
    };

    this.sendMessage = __privateMessage;

    this.clearPrivates = __clearPrivates;

    this.clearHistory = __clearHistory;

    //this.offlineMessage = __offlineMessage;  // 7.2 офлайн мессаджей больше нет

    this.getHistoryMsgRange = __syncPrivateHistory;

    this.drawHistoryMessages = __drawHistoryMessages;

    this.getUserProfile = __getProfileInformation;
    
    this.getUserPhoto = __getUserPhoto;

    this.getActiveProcess = __getActiveProcess;

    this.setDialogIdx = __setDialogIdx;

    this.setBackupIdx = function (id) {
        backupDlgId = id;
    }
}

/**
 * Created by Gifer on 22.08.2017.
 */

function MCStatusesList($rootScope) {
    var __userOnlineStatuses = {};
    var __onChange = {};

    function __clearState() {
        __userOnlineStatuses = {};
    }

    function __getAllStates() {
        return mcService.Marge({}, __userOnlineStatuses);
    }

    function __setUserState(uin, state) {
        if (uin.toString() === '0'){
            __userOnlineStatuses[uin] = mcConst.states.online;
        } else {
            __userOnlineStatuses[uin] = state === undefined ? mcConst.states.offline : parseInt(state);
        }

        mcService.setStatusForUser(uin, __userOnlineStatuses[uin]);

        return __userOnlineStatuses[uin];
    }

    function __loadUserStatuses(){
        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_get_online_users_states,
            mcConst.SessionID,

            function (states){
                if (states.Users && states.Statuses) {
                    states.Users    = states.Users.split(',');
                    states.Statuses = states.Statuses.split(',');

                    __clearState();

                    states.Users.forEach(function (uin, id) {
                        __setUserState(uin, states.Statuses[id]);
                    });

                    __setUserState(0, mcConst.states.online);

                    Object.keys(__onChange).forEach(function (name) {
                        __onChange[name](__getAllStates());
                    });

                    // $rootScope.$broadcast('allUsersOnlineStatusesChanged', [__getAllStates()]);
                }
            }
        ]);
    }

    function __getUserState(uin) {
        return __userOnlineStatuses.hasOwnProperty(uin) ? __userOnlineStatuses[uin] : mcConst.states.offline;
    }

    function __removeState(uin) {
        if (__userOnlineStatuses.hasOwnProperty(uin)){
            delete __userOnlineStatuses[uin];

            return true;
        }
    }

    function __onStatesChange(name, func) {
        __onChange[name] = func;
    }

    function __removeChangeHandler(name) {
        if (__onChange.hasOwnProperty(name)){
            delete __onChange[name];
        } else {
            console.warn('removeChangeHandler: Not found name: ' + name);
        }
    }

    // ----------------------------

    this.setUserState = __setUserState;

    this.getUserState = __getUserState;

    this.removeState  = __removeState;

    this.getAllState  = __getAllStates;

    this.clearState   = __clearState;

    this.loadUserStatuses = __loadUserStatuses;

    this.onStatesChange = __onStatesChange;

    this.removeChangeHandler = __removeChangeHandler;
}

function mcCallManager($scope, $rootScope, mcSound) {
    "use strict";

    var currentUIN   = 0;
    var DisplayName  = "";
    var _rights      = {
        QEnableVoiceCalls: false
    };

    var timer        = null;
    var mediaCall    = null;

    var mediaEvents  = new CallStates();
    var listCE       = mediaEvents.list;
    var callStates   = {
        stopCall : 1,
        reject   : 2,
        close    : 3,
        error    : 4,
        busy     : 5,
        cancel   : 6,
        timeout  : 7
    };
    var questionBox  = null;
    var callWnd      = null;
    var rejectTimer  = null;
    var cPanel       = null;
    var disconnectTimer= null;
    // var UIN          = null;

    var selfContainer  = document.createElement('video');
    var videoContainer = document.createElement('video');
    var audioContainer = document.createElement('audio');

    // ===================================

    mediaEvents.setUniversalStateEvent(function(){
        if (this !== listCE.outgoing_call_try      &&
            this !== listCE.outgoing_call_started  &&
            this !== listCE.outgoing_call_accepted &&
            this !== listCE.outgoing_call_rejected &&
            this !== listCE.incoming_call_accepted &&
            this !== listCE.incoming_call_started  &&
            this !== listCE.incoming_call_try){

            mediaEvents.clearState();
        }
    });

    // ===================================

    function hideCallWnd(all) {
        clearTimeout(rejectTimer);
        rejectTimer = null;

        clearTimeout(disconnectTimer);
        disconnectTimer = null;

        if (questionBox && questionBox.parentNode) {
            webix.modalbox.hide(questionBox);
        }

        if (all && callWnd && callWnd.parentNode){
            webix.modalbox.hide(callWnd);
        }

        mcSound.Stop();

        cPanel = null;
    }

    function sendCandidate(){
        $rootScope.$broadcast('SendCMDToServer', [ mcConst._CMD_.cs_media_ice_candidate, mcConst.SessionID].concat([].slice.call(arguments)));
    }

    function onError(err){
        console.error(err);

        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_media_call_error,
            mcConst.SessionID,

            // UIN,
            getUIN(),
            getMID(),
            mediaCall.getCallType(), //mcConst._CMD_.callType.VOICE,
            98, // "98" : "возникла ошибка во время звонка %s",
            err
        ]);

        mediaEvents.setStateEvent(listCE.outgoing_call_my_error, mcLang(498, err)); // "498":"Во время звонка возникла ошибка: %s",

        CloseCall();
    }

    function sendOffer(){
        $rootScope.$broadcast('SendCMDToServer', [ mcConst._CMD_.cs_media_offer, mcConst.SessionID].concat([].slice.call(arguments)));
    }

    function sendAnswer() {
        $rootScope.$broadcast('SendCMDToServer', [ mcConst._CMD_.cs_media_answer, mcConst.SessionID].concat([].slice.call(arguments)));
    }

    function showMedia(){ // videoContainer
        if (!mediaCall.complete){
            hideCallWnd(true);

            timer.Start();

            mediaCall.complete = true;

            mcSound.Stop(mcSound.Sounds.Receive);

            cPanel = new Create_cPanel(mediaCall, mediaCall.useVideo, mediaCall.receiveScreen, videoContainer);

            if (!mcService.checkRights(mcConst._CMD_.RS.QPrivAllowDisableCameraDuringCalls)) {
                cPanel.cameraSwitch.disable();
            }

            if (!mcService.checkRights(mcConst._CMD_.RS.QPrivAllowMuteSoundDuringCalls)) {
                cPanel.microphoneSwitch.disable();
            }

            if (onShowMedia){
                onShowMedia({
                    useVideo     : mediaCall.useVideo,
                    receiveScreen: mediaCall.receiveScreen && !mediaEvents.isMyCall,

                    video      : videoContainer,
                    audio      : audioContainer,
                    mirror     : selfContainer,
                    cPanel     : cPanel
                });
            }
        }
    }

    function onDisconnected(uin){
        webix.message("Call disconnected, try restore");

        if (!disconnectTimer) {
            disconnectTimer = setTimeout(function () {
                disconnectTimer = null;

                CloseCall();
            }, 30 * 1000);
        }
    }

    function onRestoreConnect() {
        webix.message("Call restored");

        clearTimeout(disconnectTimer);
        disconnectTimer = null;
    }

    // ===================================

    function setInfo(info){//}, video, share, MID) {
        mediaCall.setCallInfo(info.CallType, info.MID);//video, share, MID);
        mediaCall.addIceServers(mcService.makeIceServers(info, $rootScope.isWebClient), $rootScope.isWebClient ? "relay" : mcConst.ClientSettings.IceTransportPolicy);
    }

    function hideCallQuestion() {
        hideCallWnd(true);
    }

    // -- incoming call --
    function callQuestions (info, done){ // receive call
        function answer(type){
            if (location.protocol === "http:") {
                $rootScope.$broadcast('SendCMDToServer', [
                    mcConst._CMD_.cs_media_call_error,
                    mcConst.SessionID,

                    // UIN,
                    getUIN(),
                    info.MID,
                    type, // ? 1 : (info.Share ? 2 : 0), //"Type"      : 0,   // (8.1+) тип звонка (0 - voice, 1 - video, 2 - screen sharing)
                    278,                         // (8.1+) номер ошибки, которая возникла во время звонка
                    ""                           // (8.1+) текстовое описание ошибки, если оно есть
                ]);

                CloseCall(info.CallType);

                console.log("HTTP protocol, can't receive call");
            } else {
                setInfo(info); //, video, info.Share, info.MID);

                $rootScope.$broadcast('SendCMDToServer', [
                    mcConst._CMD_.cs_media_call_accept,
                    mcConst.SessionID,

                    // UIN,
                    getUIN(),
                    type,
                    info.MID,
                    $rootScope.clientSysInfo ? $rootScope.clientSysInfo.Client : mcConst._CMD_.ClientType.WEB_CHAT
                ]);

                setTimeout(function () {
                    $rootScope.$broadcast('SendCMDToServer', [
                        mcConst._CMD_.cs_media_ready,
                        mcConst.SessionID,
                        // info.UIN,
                        getUIN(),
                        info.MID
                    ]);
                }, 50);
            }
        }

        function reject(state){
            mcSound.Stop(mcSound.Sounds.Receive);

            mcSound.PlaySound(mcSound.Sounds.Reject);

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_media_call_reject,
                mcConst.SessionID,
                // UIN,
                getUIN(),
                state,
                getMID()
            ]);

            mediaEvents.setStateEvent(state, mcLang(506)); // "506":"Вы отказались принять звонок",
        }

        var btns = 0;

        switch (info.CallType){
            case mcConst._CMD_.callType.VIDEO:
                       //"22" : "Видео",    "21" : "Голос",     "33" : "Отмена",
                btns = [mcService.Lang(22), mcService.Lang(21), mcService.Lang(33)];
            break;

            case mcConst._CMD_.callType.VOICE:
                       //"21" : "Голос",     "33" : "Отмена",
                btns = [mcService.Lang(21),  mcService.Lang(33)];

            break;

            case mcConst._CMD_.callType.SHARING:
                       //"659":"Экран",      "33" : "Отмена",
                btns = [mcService.Lang(659), mcService.Lang(33)];
            break;
        }

        mediaEvents.isMyCall = false;
        mediaEvents.setCustomCallEvent(listCE.incoming_call_try);

        // UIN = info.UIN;
        currentUIN = info.UIN;

        // console.log("Call from UIN: " + UIN);
        console.log("Call from UIN: " + getUIN());

        questionBox = webix.modalbox({
            buttons: btns,
            text   : mcService.Lang(39) + info.Nick, // "39" : "Входящий звонок",
            width  : "400px",
            callback: function(_result){
                var variant = 2;

                if (_result === true){
                    variant = info.CallType === mcConst._CMD_.callType.VIDEO ? 1 : 0;
                } else
                if (_result === false){
                    variant = info.CallType === mcConst._CMD_.callType.VIDEO ? 2 : 1;
                } else {
                    variant = parseInt(_result);
                }

                switch(variant){
                    case 0:
                        answer(info.CallType);

                        if (done) done();
                    break;

                    case 1:
                        if (info.CallType === mcConst._CMD_.callType.VIDEO){
                            info.CallType = mcConst._CMD_.callType.VOICE;
                            
                            answer(info.CallType);

                            if (done) done();
                        } else {
                            reject(listCE.server_state_rejected);
                        }
                    break;

                    case 2:
                        reject(listCE.server_state_rejected);
                    break;
                }
            }
        });
    }

    // -- outgoing call --
    function myCall(_uin, callType/*, onReject*/){
        if (location.protocol === "http:") {
            $rootScope.$broadcast('ErrorMsg', [278, mcService.Error(278), [], function(){
                // onReject && onReject();
                _hideStopCall();
            }]);

            console.log("HTTP protocol, can't receive call");
        } else {
            mediaEvents.isMyCall = true;

            mediaEvents.setCustomCallEvent(listCE.outgoing_call_try);

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_media_call,
                mcConst.SessionID,

                _uin,
                callType,
                $rootScope.clientSysInfo ? $rootScope.clientSysInfo.Client : mcConst._CMD_.ClientType.WEB_CHAT
            ]);
        }
    }

    function reject_outgoing_call(timeout, state){
        hideCallWnd(true);

        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_media_call_reject,
            mcConst.SessionID,
            getUIN(),
            state,
            getMID()
        ]);

        mediaEvents.setStateEvent(state, mcLang(507));  //"507":"Исходящий звонок прерван",

        CloseCall(timeout ? callStates.timeout : callStates.cancel);

        mcSound.PlaySound(mcSound.Sounds.Reject);

        // if (onReject) onReject();
        _hideStopCall();
    }

    function setTimerView (view) {
        timer && timer.setView(view);
    }

    function CloseCall(callType, params){
        hideCallWnd(true);

        if (nowCalling()) {

            if (onCloseCall){
                onCloseCall({
                    MID          : getMID(),
                    // receiveScreen: mediaCall.receiveScreen,
                    // useVideo     : mediaCall.useVideo,

                    cPanel       : cPanel
                });
            }

            var time = timer.Stop();

            switch (callType){
                case callStates.stopCall:
                    if (mediaEvents.isMyCall){
                        mediaEvents.setStateEvent(listCE.outgoing_call_my_close, mcLang(495, mcService.MyFormatTime(time, time < 3600))); // "495":"Исходящий звонок от меня, продолжительность %s",
                    } else {
                        mediaEvents.setStateEvent(listCE.incoming_call_my_close, mcLang(494, [params.DisplayName, mcService.MyFormatTime(time, time < 3600)])); // "494":"Входящий звонок от %s, продолжительность %s",
                    }
                    break;

                case callStates.reject:
                    if (mediaEvents.isMyCall){
                        mediaEvents.setStateEvent(listCE.outgoing_call_rejected, mcLang(496)); // "496":"Пользователь отказался принять ваш звонок",
                    } else {
                        mediaEvents.setStateEvent(listCE.incoming_call_rejected, mcLang(500)); // "500":"Пропущен входящий звонок",
                    }
                    break;

                case callStates.close:
                    if (mediaEvents.isMyCall){
                        mediaEvents.setStateEvent(listCE.outgoing_call_remote_close, mcLang(495, mcService.MyFormatTime(time, time < 3600))); // "495":"Исходящий звонок от меня, продолжительность %s",
                    } else {
                        mediaEvents.setStateEvent(listCE.incoming_call_remote_close, mcLang(494, [params.DisplayName, mcService.MyFormatTime(time, time < 3600)])); // "494":"Входящий звонок от %s, продолжительность %s",
                    }
                    break;

                case callStates.error:
                    mediaEvents.setStateEvent(
                        mediaEvents.isMyCall
                            ? listCE.outgoing_call_remote_error
                            : listCE.incoming_call_remote_error, mcLang(499)); // "499":"Во время звонка у вашего собеседника возникла ошибка",
                    break;

                case callStates.busy:
                    mediaEvents.setStateEvent(listCE.outgoing_call_busy, mcLang(501)); // "501":"Исходящий звонок от меня, занято.",
                    break;

                /*
                 case Self.callStates.cancel:    // todo: add text
                 if (mediaEvents.isMyCall){
                 mediaEvents.setStateEvent(listCE.outgoing_call_cancelled);
                 } else {
                 mediaEvents.setStateEvent(listCE.incoming_call_cancelled);
                 }
                 break;

                 case Self.callStates.timeout:   // todo: add text
                 if (mediaEvents.isMyCall){
                 mediaEvents.setStateEvent(listCE.outgoing_call_cancelled_by_timeout);
                 } else {
                 mediaEvents.setStateEvent(listCE.incoming_call_cancelled_by_timeout);
                 }
                 break;
                 */
            }

            // UIN = null;
            currentUIN = 0;

            //webix.message(mcLang(493)); // "493":"Сеанс связи закончен...",

            mediaCall.complete = false;
            mediaCall.hangUp();
        }

        mediaEvents.clearState();

        if (cPanel) {
            cPanel.destructor();
            cPanel = null;
        }
    }

    function preShow(callType){
        hideCallWnd();

        if (mediaEvents.isMyCall){
            mediaEvents.setCustomCallEvent(listCE.outgoing_call_accepted);
        } else {
            mediaEvents.setCustomCallEvent(listCE.incoming_call_started);
        }

        if (callType === mcConst._CMD_.callType.VIDEO || [mcConst._CMD_.callType.VIDEO, mcConst._CMD_.callType.SHARING].indexOf(mediaCall.getCallType()) >= 0) {
            mediaCall.setMirror(selfContainer);
            mediaCall.setCallee(videoContainer);
        } else {
            mediaCall.setCallee(audioContainer);
        }
    }

    function setScreenId(id){
        mediaCall && mediaCall.setScreenId(id);
    }

    function nowCalling() {
        return mediaEvents.currentState() !== listCE.free;
    }

    function getUIN() {
        return currentUIN;
    }

    function getMID() {
        return mediaCall.MID;
    }

    function onShowMedia(data){
        $rootScope.$broadcast(window._messages_.chatFrame.showVideoFrame, [data]);

        if ($scope.toolCallTimer) {
            $scope.toolCallTimer.show();

            setTimerView($scope.toolCallTimer.getNode().firstChild);
        }
    }

    function onCloseCall(data){
        _hideStopCall();

        console.log('Exit from MID: ' + data.MID);

        $rootScope.$broadcast(window._messages_.chatFrame.hideVideoFrame,  [data]);
        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_media_exit_from_mid,
            mcConst.SessionID,

            data.MID
        ]);
    }

    // ==================================================================

    function _getScreenThumbs(cb) {
        if (location.protocol === "file:") {
            var desktopCapturer = require('electron').desktopCapturer;
            var res = [];

            desktopCapturer.getSources({ types: ['screen'] }).then(function (sources) {
                sources.forEach(function (source) {
                    res.push({
                        id : source.id,
                        img: source.thumbnail.toDataURL()
                    });
                });

                cb(res);
            });
        } else  {
            cb(null);
        }
    }

    function _showStopCall() {
        if ($scope.startVideoCall){
            !$rootScope.isWebClient && $scope.startShareCall.hide();
            $scope.startVideoCall.hide();
            $scope.startAudioCall.hide();
        }

        $scope.stopCall && $scope.stopCall.show();
        // $scope.toolCallTimer && $scope.toolCallTimer.show();
    }

    function _hideStopCall() {
        if ($scope.stopCall){
            $scope.stopCall.hide();
            $scope.toolCallTimer.hide();

            _rights.QEnableVoiceCalls && !$rootScope.isWebClient && $scope.startShareCall.show();
            _rights.QEnableVideoCalls && $scope.startVideoCall.show();
            _rights.QEnableVoiceCalls && $scope.startAudioCall.show();
        }
    }

    function _startVoiceCall() {
        currentUIN = $rootScope.GetChatID();

        if (getUIN() != mcConst.ElisaUIN){
            _showStopCall();

            myCall(getUIN(), mcConst._CMD_.callType.VOICE/*false, false*//*, _hideStopCall*/);
        }
    }

    function _startVideoCall() {
        currentUIN = $rootScope.GetChatID();

        if (getUIN() != mcConst.ElisaUIN) {
            _showStopCall();

            myCall(getUIN(), mcConst._CMD_.callType.VIDEO/*true, false*//*, _hideStopCall*/);
        }
    }

    function _startShareCall() {
        currentUIN = $rootScope.GetChatID();

        if (getUIN() != mcConst.ElisaUIN) {
            _getScreenThumbs(function (scrList){
                if (scrList){
                    var imageList = "";

                    scrList.forEach(function (item){
                        imageList +=
                            "<div>" + item.id + "</div>" +
                            "<img class='selectScreen finger' id='___" + item.id + "' src='" + item.img + "' style='max-width: 100%'><br>";
                    });

                    var wnd = webix.ui({
                        view    : "window",
                        position: "center",
                        modal   : true,
                        width   : 260,
                        height  : 300,
                        head    : mcLang(724), // "724":"Выберите экран",
                        body    : { rows: [
                            { template: imageList, borderless: true, scroll: "y", css: "byCenter", onClick: {
                                "selectScreen": function (ev){
                                    var trash  = ev.target;
                                    var scrIdx = trash.getAttribute('id').replace("___", "");

                                    _showStopCall();

                                    setScreenId(scrIdx);
                                    
                                    myCall(getUIN(), mcConst._CMD_.callType.SHARING/*false, true*//*, _hideStopCall*/);

                                    wnd.hide();
                                    wnd.destructor();
                                }
                            }},
                            { view: "button", value: mcLang(12), click: function(){ //"12" : "Закрыть",
                                wnd.hide();
                                wnd.destructor();
                            }}
                        ]}
                    });

                    wnd.show();
                }
            });
        }
    }

    function _stopCall(reason, opts){
        if (nowCalling()) {

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_media_call_close,
                mcConst.SessionID,
                // getUIN(),
                getUIN(),
                getMID()
            ]);

            CloseCall(reason !== void(0) ? reason : callStates.stopCall, opts || {
                DisplayName: DisplayName
            });

            mcSound.PlaySound(mcSound.Sounds.End);

            if (!$rootScope.isWebClient) {
                $rootScope.$broadcast(window._messages_.clientData.toggleKioskMode, [false]);
            }
        }

        if ($rootScope.GetChatType() === $rootScope.chatAliases.UIN && $rootScope.GetChatID() != 0){
            _showBtns();
        } else {
            _hideBtns();
        }
    }

    function _changeConnectStatus(status, uin) {
        // console.log(arguments);
        
        if (status !== void(0)) {
            var Statuses = mcService.getStateStatuses(status);

            if (mcService.isObject(Statuses) || (!isNaN(parseInt(status)) && getUIN() == uin)) {
                var notInOfflineList = mcService.inArrayNoStrict(getUIN(), Statuses.Offline) === -1;
                var inOnlineList     = mcService.inArrayNoStrict(getUIN(), Statuses.Online) >= 0;

                // юзер перелогинился и нам не ришел его статус офлайн, а сразу онлайн - отключаем звонок
                if ((inOnlineList && nowCalling() && getUIN() == uin) ) {
                    _stopCall();
                }
                
                if (inOnlineList || (notInOfflineList && status !== mcConst.states.offline && !isNaN(parseInt(status)))) {
                    !$rootScope.isWebClient && $scope.startShareCall.enable();
                    $scope.startVideoCall.enable();
                    $scope.startAudioCall.enable();
                } else
                if ((!notInOfflineList || status === mcConst.states.offline) && $scope.startAudioCall) { // юзер стал офлайн
                    !$rootScope.isWebClient && $scope.startShareCall.disable();
                    $scope.startVideoCall.disable();
                    $scope.startAudioCall.disable();

                    _stopCall();
                }
            }
        }
    }

    function _showBtns() {
        if (nowCalling()){
            $scope.callBtsWrap.hide();
            $scope.stopCall.show();
        } else {
            $scope.callBtsWrap.show();
            $scope.stopCall.hide();
        }
    }

    function _hideBtns() {
        if (nowCalling()){
            $scope.callBtsWrap.hide();
            $scope.stopCall.show();
        } else {
            $scope.callBtsWrap.hide();
            $scope.stopCall.hide();
        }
    }

    // function getCallType(info) {
    //     return info.Video ? mcConst._CMD_.callType.VIDEO : (info.Share ? mcConst._CMD_.callType.SHARING : mcConst._CMD_.callType.VOICE);
    // }

    function _getCallBtns(){
        var view = [
            { view: "label", label: "00:00", id: "toolCallTimer", hidden: true, width: 50, css: "byCenter"},

            { id: "callBtsWrap", hidden: true, cols: [
                    { width: 4, css: "noBGColor"},

                    { view: "button", type: "icon", id: "toolShareCall", click: _startShareCall, icon: "desktop", css: "icoButton byCenter button_primary icnBtnPd03 noMargin", width: 35, hidden: $rootScope.isWebClient},
                    { view: "button", type: "icon", id: "toolAudioCall", click: _startVoiceCall, icon: "phone",   css: "icoButton byCenter button_primary icnBtnPd03 noMargin", width: 35}, // "21" : "Позвонить",
                    { view: "button", type: "icon", id: "toolVideoCall", click: _startVideoCall, icon: "video-camera", css: "icoButton byCenter button_success icnBtnPd03 noMargin", width: 35}, // "22" : "Видеозвонок",

                    { width: 4, css: "noBGColor"}
                ]},

            { view: "button", type: "icon", id: "toolStopCall",  click: _stopCall, icon: "phone", label: mcLang(23), css: "icoButton byCenter button_danger button_raised icnBtnPd03 fa-rotate-90", width: 35, hidden: true}, // "23" : "Завершить звонок",
        ];

        $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
            _rights.QEnableVoiceCalls      = mcService.checkRights(mcConst._CMD_.RS.QEnableVoiceCalls);
            _rights.QEnableVideoCalls      = mcService.checkRights(mcConst._CMD_.RS.QEnableVideoCalls);

            !$rootScope.isWebClient && $scope.startShareCall && $scope.startShareCall[_rights.QEnableVoiceCalls     ? 'show': 'hide']();

            $scope.startAudioCall && $scope.startAudioCall[_rights.QEnableVoiceCalls     ? 'show': 'hide']();
            $scope.startVideoCall && $scope.startVideoCall[_rights.QEnableVideoCalls     ? 'show': 'hide']();
        });

        return view;
    }

    function _initCallBtns(){
        $scope.startAudioCall = $$("toolAudioCall");
        $scope.startVideoCall = $$("toolVideoCall");
        $scope.startShareCall = $$("toolShareCall");
        $scope.stopCall       = $$("toolStopCall");
        $scope.callBtsWrap    = $$("callBtsWrap");
        $scope.toolCallTimer  = $$("toolCallTimer");

        $scope.startShareCall.getNode().title = mcLang(604); // "604":"Демонстрация экрана",
        $scope.startVideoCall.getNode().title = mcLang(606); // "606":"Видео звонок",
        $scope.startAudioCall.getNode().title = mcLang(605); // "605":"Голосовой звонок",
        $scope.stopCall      .getNode().title = mcLang(23);  // "23" :"Завершить звонок",
    }

    function _setUserInfo(uin, name) {
        currentUIN  = uin;
        DisplayName = name;
    }

    // ============================================================================
    // ============================================================================

    var _msg = window._messages_.callManager = {
        startAudioCall              : "startAudioCall",
        startVideoCall              : "startVideoCall",
        startShareCall              : "startShareCall",
        stopCurrentCall             : "stopCurrentCall",

        HideStopCall                : "HideStopCall",
        mediaCallMessage            : "mediaCallMessage",
        onlineOfflineUsers          : "onlineOfflineUsers",
        changeCallState             : "changeCallState",

        sc_media_call               : 'sc_media_call',
        sc_media_call_accept        : 'sc_media_call_accept',
        sc_media_call_error         : 'sc_media_call_error',
        sc_media_ready              : 'sc_media_ready',
        sc_media_offer              : 'sc_media_offer',
        sc_media_answer             : 'sc_media_answer',
        sc_media_ice_candidate      : 'sc_media_ice_candidate',
        sc_media_incoming_call_aby_cid: 'sc_media_incoming_call_accepted_by_another_cid',
        sc_media_call_start_ok      : 'sc_media_call_start_ok',

        getCallBtns                 : 'getViewCallManager',
        initCallBtns                : 'initCallBtns',
        setUserInfo                 : 'setUserInfo',
        showCallBtns                : 'showCallBtns',
        hideCallBtns                : 'hideCallBtns'
    };

    $scope.$on(_msg.getCallBtns,       _getCallBtns);
    $scope.$on(_msg.showCallBtns,      _showBtns);
    $scope.$on(_msg.hideCallBtns,      _hideBtns);
    $scope.$on(_msg.startAudioCall,    _startVoiceCall);
    $scope.$on(_msg.startVideoCall,    _startVideoCall);
    $scope.$on(_msg.startShareCall,    _startShareCall);
    $scope.$on(_msg.stopCurrentCall,   _stopCall);
    $scope.$on(_msg.HideStopCall,      _hideStopCall);
    $scope.$on(_msg.initCallBtns,       function(e, args){
        _initCallBtns.apply(null, args);
    });
    $scope.$on(_msg.setUserInfo,        function(e, args){
        _setUserInfo.apply(null, args);
    });
    $scope.$on(_msg.onlineOfflineUsers, function(e, args){
        _changeConnectStatus.apply(null, args);
    });
    $scope.$on(_msg.changeCallState,    function(e, args){
        _changeConnectStatus.apply(null, args);
    });
    $scope.$on(_msg.mediaCallMessage,   function(e, args){
        var callMessage = args[0];
        var callInfo    = mcService.StringToObj(callMessage.Msg);         // callMessage.Msg => {"UINFrom":16222,"Duration":15,"UINTo":13041,"Type":1,"State":9}
        var uin         = callInfo.UINFrom != mcConst.UserInfo.UIN ? callInfo.UINFrom : callInfo.UINTo;

        if (nowCalling() && uin == getUIN() && !callMessage.History) {
            _stopCall(callStates.close, {
                DisplayName: callMessage.Nick,
                isMyCall   : callInfo.UINFrom == mcConst.UserInfo.UIN
            });
        }
    });

    // ============================================================================
    // ============================================================================

    $scope.$on(_msg.sc_media_call,         function Receive_Call  (e, args) {
        var obj = args[0];

        if (!nowCalling()) {
            mcSound.PlaySound(mcSound.Sounds.Receive);

            if (!$rootScope.isWebClient){
                $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                    mcConst._CMD_.ce_show_on_top
                ]);
            }

            callQuestions(obj, function () {
                $rootScope.$broadcast('OpenPrivate', [obj.UIN]);

                _showBtns();
                _showStopCall();
            });
        }
    });
    $scope.$on(_msg.sc_media_call_accept,  function Receive_Accept(e, args) { // my call
        var obj = args[0];

        setInfo(obj);//, obj.Video, false, obj.MID);
        preShow(obj.CallType);
    });
    $scope.$on(_msg.sc_media_call_start_ok,function Can_start_call(e, args) { // my call
        // "UIN"              : 17,   // кому нужно позвонить
        // "UINOriginal"      : 17,
        // "CallTypeOriginal" : 0,    // тип звонка
        // "CallType"         : 0,    // тип звонка
        //                            // MEDIA_CALL_TYPE_VOICE          = 0
        //                            // MEDIA_CALL_TYPE_VIDEO          = 1
        //                            // MEDIA_CALL_TYPE_SCREEN_SHARING = 2
        // "MID"              : 82374827346, // MediaID
        // "FullScreen"       : true,        // разворачивать окно звонка на весь экран (только для VIDEO/SCREEN_SHARING)
        // "ScreenID"         : "screen:0:0" // номер экрана, только для SCREEN_SHARING
        var obj = args[0];

        currentUIN = obj.UIN;

        mediaCall.Share = obj.CallType === mcConst._CMD_.callType.SHARING;    ///////////////////`

        if (mediaCall.Share){
            mediaCall.setScreenId(obj.ScreenID);
        }

        mcSound.Play(mcSound.Sounds.Call);

        callWnd = webix.alert({
            type: "alert-warning",
            ok  : mcLang(509), // "509":"Завершить",
            text: mcLang(510), // "510":"Ожидаем овет...",
            callback: function () {
                reject_outgoing_call(false, listCE.outgoing_call_cancelled);
            }
        });

        rejectTimer = setTimeout(function () {
            reject_outgoing_call(true, listCE.outgoing_call_cancelled_by_timeout);
        }, 60000);
    });
    $scope.$on(_msg.sc_media_call_error,   function Receive_Error (e, args) {
        var _data = args[0];
        var err   = mcService.isObject(_data) ? JSON.stringify(_data) : _data;

        console.warn('Media error code: ' + err);

        if (nowCalling()){
            CloseCall(callStates.error, err);

            mcSound.PlaySound(mcSound.Sounds.End);
        }
    });
    $scope.$on(_msg.sc_media_ready,        function Receive_Ready (e, args) {
        if (args[0].UIN == getUIN()){    // irrorContainer undefined! Use "setMirror" first!
            // recvReady(args[0].UIN);
            mediaCall.recvReady(args[0].UIN);
        }
    });
    $scope.$on(_msg.sc_media_offer,        function Receive_Offer (e, args) { // call to me
        preShow();
        // recvOffer(args[0]);
        mediaCall.recvOffer(args[0]);
    });
    $scope.$on(_msg.sc_media_answer,       function Receive_Answer(e, args) {
        // recvAnswer(args[0]);
        mediaEvents.setCustomCallEvent(listCE.outgoing_call_started);
        mediaCall.recvAnswer(args[0]);
    });
    $scope.$on(_msg.sc_media_ice_candidate,function Receive_Ice_Candidate(e, args) {
        // recvCandidate(args[0]);
        mediaCall.recvCandidate(args[0]);
    });
    $scope.$on(_msg.sc_media_incoming_call_aby_cid,function Receive_accepted_by_another_cid() {
        hideCallQuestion();
    });

    // ============================================================================
    // ============================================================================

    function _prepare(_timerView){
        timer     = new mcService.MyTimer(_timerView);
        mediaCall = new MediaCall();

        mediaCall.complete       = false;
        mediaCall.sendAnswer     = sendAnswer;
        mediaCall.sendOffer      = sendOffer;
        mediaCall.sendCandidate  = sendCandidate;
        mediaCall.onError        = onError;
        mediaCall.showMedia      = showMedia;
        mediaCall.onDisconnected = onDisconnected;
        mediaCall.onRestoreConnect = onRestoreConnect;

        videoContainer.id        = mcConst.videoStreamName;
        videoContainer.autoplay  = "autoplay";
        videoContainer.className = "VideoCaller";

        audioContainer.id        = mcConst.audioStreamName;
        audioContainer.autoplay  = "autoplay";
        audioContainer.className = "AudioCaller";

        selfContainer.id         = mcConst.mirrorStreamName;
        selfContainer.autoplay   = "autoplay";
        selfContainer.className  = "myVideo";
        selfContainer.muted      = "muted";

        // recvReady     = mediaCall.recvReady;
        // recvOffer     = mediaCall.recvOffer;
        // recvCandidate = mediaCall.recvCandidate;
        // recvAnswer    = function () {
        //     mediaEvents.setCustomCallEvent(listCE.outgoing_call_started);
        //     mediaCall.recvAnswer.apply(null, arguments);
        // };
        // return mediaCall;
    }

    _prepare();
}

function McMessageRender($scope, $rootScope, mcPlaySound, mcEmojiList) {
    "use strict";

    var Self         = this;
    var __myMessages = {
        UIN        : -1,
        IDMyMsgGot : 0,
        IDMyMsgRead: 0
    };
    var listCE       = new CallStates().list;
    var loadImages   = new McLoadingImages($rootScope);
    var emojiList    = mcEmojiList.getPlainList();
    var delMyMsgList = {};
    var loadAvatars  = [];
    var avatarTimer  = null;

    // ---------------------------------------------

    var RENDER_MSG = new function () {
        var bufferMsg       = [];
        var bufferHistory   = [];
        var newMsgTimer     = null;
        var historyMsgTimer = null;

        var constList = {
            normal: 1,
            custom: 2,
            kanban: 3,
            redirect: 4,
            files : 5,
            forum : 6
        };

        function historyRender(noScrollDown, rnd) {
            if (bufferHistory.length){
                var buf   = bufferHistory;
                var frame = buf[0].frame;
                var scrollHeight = buf[0].target.getScrollH();
                var _container = null;

                bufferHistory   = [];

                buf.forEach(function (msg, i) {
                    var _br = null;


                    switch (msg.__msgType) {
                        case constList.normal:
                            if (!msg.same) {
                                _br = document.createElement('span');
                                _br.className = "spanBr";

                                msg.target.addToHead(_br);
                            } else

                            if (msg.target.getByAttrFirst('postDate') === msg.date ){
                                var fMSG = msg.target.getFirstMsg();

                                fMSG.removeChild(msg.target.getFirstMsg().firstChild);
                                
                                if (mcConst.ClientSettings.GeneralSeparateMyMessages && fMSG.firstChild !== null) {
                                    fMSG.firstChild.className += " msgSeparator";
                                }
                            } else {
                                _br = document.createElement('span');
                                _br.className = "spanBr";

                                msg.target.addToHead(_br);
                            }

                            msg.target.addToHead(msg.div);

                            if (msg.onAdd){
                                msg.onAdd();
                            }

                            if (msg.loadThmb) {
                                loadImages.loadThumbs(
                                    [
                                        msg.info[0],
                                        msg.info[1],
                                        msg.msgIdx
                                    ],

                                    function (){
                                        Self.scrollDown && Self.scrollDown(frame, false, !msg.History, msg.History && msg.noScrollDown)
                                    }
                                );
                            } else
                            if (msg.loadThmbNew) {
                                loadImages.loadThumbs(
                                    [
                                        msg.loadThmbNew.Hash,
                                        msg.loadThmbNew.FileName,
                                        msg.msgIdx
                                    ],

                                    function (){
                                        Self.scrollDown && Self.scrollDown(frame, false, !msg.History, msg.History && msg.noScrollDown)
                                    }
                                );
                            }
                        break;

                        case constList.custom: case constList.files: case constList.kanban: case constList.redirect: case constList.forum:
                            _br = document.createElement('span');
                            _br.className = "spanBr";

                            msg.target.addToHead(_br, true);
                            msg.target.addToHead(msg.div);

                            if (msg.onAdd){
                                msg.onAdd();
                            }
                        break;
                    }

                    _container = msg.target;
                });

                scrollHeight = buf[buf.length - 1].target.getScrollH() - scrollHeight;

                historyRender(noScrollDown, rnd);
                
                historyMsgTimer = null;

                _container.uploadHistoryTimer = null;

                // if ($rootScope.GetChatTypeID() === frame) {
                    Self.scrollDown(frame, false, false, noScrollDown, scrollHeight);
                // }
            } else {
                // window.___native_console.timeEnd("messageH" + rnd);
            }
        }

        function msgRender(rnd) {
            if (bufferMsg.length){
                var buf   = bufferMsg;
                var frame = buf[0].frame;

                bufferMsg   = [];

                buf.forEach(function (msg, i) {
                    var _br = null;

                    switch (msg.__msgType) {
                        case constList.normal:
                            if (!msg.same) {
                                _br = document.createElement('span');
                                _br.className = "spanBr";

                                msg.target.addToFoot(_br);
                            }

                            msg.target.addToFoot(msg.div);

                            if (msg.onAdd){
                                msg.onAdd();
                            }

                            if (msg.loadThmb) {
                                loadImages.loadThumbs(
                                    [
                                        msg.info[0],
                                        msg.info[1],
                                        msg.msgIdx
                                    ],

                                    function (){
                                        Self.scrollDown && Self.scrollDown(frame, false, !msg.History, msg.History && msg.noScrollDown)
                                    }
                                );
                            } else
                            if (msg.loadThmbNew) {
                                loadImages.loadThumbs(
                                    [
                                        msg.loadThmbNew.Hash,
                                        msg.loadThmbNew.FileName,
                                        msg.msgIdx
                                    ],

                                    function (){
                                        Self.scrollDown && Self.scrollDown(frame, false, !msg.History, msg.History && msg.noScrollDown)
                                    }
                                );
                            }
                        break;

                        case constList.custom: case constList.files: case constList.kanban: case constList.redirect: case constList.forum:
                            _br = document.createElement('span');
                            _br.className = "spanBr";

                            msg.target.addToFoot(_br);
                            msg.target.addToFoot(msg.div);

                            if (msg.onAdd){
                                msg.onAdd();
                            }
                        break;
                    }
                });

                msgRender(rnd);

                newMsgTimer = null;

                // if ($rootScope.GetChatTypeID() === frame) {
                    Self.scrollDown(frame, false, true);
                // }
            } else {
                // window.___native_console.timeEnd("message" + rnd);
            }
        }

        this.cost = constList;

        this.getLastMessage = function () {
            return bufferMsg.length ? bufferMsg[bufferMsg.length - 1] : null;
        };

        this.getLastHistoryMsg = function () {
            return bufferHistory.length ? bufferHistory[bufferHistory.length - 1] : null;
        };

        this.message = function (msg, total, type) {
            total.__msgType = type;

            // var rnd = mcService.RandomHash(4);

            if (msg.History || msg.UpSync){
                bufferHistory.push(total);

                // window.___native_console.time("messageH" + rnd);

                if (historyMsgTimer){
                    clearTimeout(historyMsgTimer);
                }

                historyMsgTimer = setTimeout(function (){
                    historyRender(msg.noScrollDown);
                }, 50);
            } else {
                bufferMsg.push(total);

                // window.___native_console.time("message" + rnd);

                if (newMsgTimer === null){
                    newMsgTimer = setTimeout(function () {
                        msgRender();
                    }, 10);
                }
            }
        }
    };

    // ---------------------------------------------

    function __getUinWith(msg){
        return ((mcConst.UserInfo.UIN == msg.UINFrom) ? msg.UINTo : msg.UINFrom);
    }

    function __showNotify(notI, text, nick, isConf, id, noBlink){
        text = (text || "").replace(/<br\/>/g, "\n");

        var textForNotify = text.slice(0, 100) + ((text.length > 100) ? '...' : " " );
        var itemID        = (isConf ? 'UID-' : 'UIN-') + id;
        var title         = nick ? (nick.slice(0, 30) + ((nick.length > 30) ? '..' : " " ) + ":") : " ";

        if (notI){
            if (($rootScope.isWebClient || mcConst.ClientSettings.SoundsSndChat) && isConf){
                mcPlaySound.PlaySound(mcPlaySound.Sounds.ConfMsg, true);
            } else
            if (($rootScope.isWebClient || mcConst.ClientSettings.SoundsSndPrivate) && !isConf){
                mcPlaySound.PlaySound(mcPlaySound.Sounds.Private, true);
            }

            if ($rootScope.isWebClient){
                if ($rootScope.GetChatTypeID() !== itemID){
                    mcService.CreateNotification({
                        title : title,
                        body  : textForNotify,
                        tag   : mcConst.UserInfo.UIN + "_" +  mcConst.UserInfo.DisplayName,
                        timeOut : 5000,
                        click : function onClickOnNotify(){
                            if (isConf){
                                $rootScope.$broadcast('OpenConf', [id]);
                            } else {
                                $rootScope.$broadcast('OpenPrivate', [id]);
                            }
                        }
                    });

                    console.log(title + "\n" + textForNotify);
                }
            } else

            if (( isConf && ($rootScope.isWebClient || mcConst.ClientSettings.EventsTrayChMsg == 1)) ||
                (!isConf && ($rootScope.isWebClient || mcConst.ClientSettings.EventsTrayPvMsg == 1))){
                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_show_notify,

                    title,
                    textForNotify,
                    itemID,
                    noBlink
                ]);
            }
        }
    }

    function __parseLink(source) {
        return source ? mcService.ReplaceURLs(source, { callback: function(_text, _link){
            var res = "";

            if (/png|jpg|jpeg$/.test(_link)){
                res = '<br/><img class="thumbImage" src="' + _link + '">';
            }

            return _link ? '<a href="' + _link + '" title="' + _link + '" target="_blank">' + (res || _text) + '</a>' : _text;
        }}) : "";
    }

    function __removeMSG(msg) {
        var Idx = mcService.StringToObj(msg.Msg);

        if (Idx.Idx && ((Idx.Type == 0 || (Idx.Type == 1 && msg.UIN == mcConst.UserInfo.UIN)) || !Idx.Type)){
            Idx = Idx.Idx;
        }

        var frameID = $rootScope.GetChatTypeID();
        var isConf  = !!msg.UID;
        var message = document.getElementById(isConf ? "conf-" + msg.UID + "-" + Idx : "priv-" + (msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom) + "-" + Idx);

        if (msg.History && !message && Idx && msg.UIN == mcConst.UserInfo.UIN){
            if (!delMyMsgList[frameID]){
                delMyMsgList[frameID] = {};
            }

            delMyMsgList[frameID][Idx] = 1;
        }

        if (Idx && message){
            if (message.nextSibling && message.nextSibling.className.indexOf("message") === 0 && message.childElementCount === 2) {
                message.nextSibling.insertBefore(message.firstChild, message.nextSibling.firstChild);
            }

            if ((!message.previousSibling && message.nextSibling && message.nextSibling.className === "spanBr") ||
                (message.previousSibling && message.previousSibling.className === "spanBr" && message.nextSibling && message.nextSibling.className === "spanBr")) {
                message.parentElement.removeChild(message.nextSibling);
            }

            message.parentElement.removeChild(message);
        }
    }

    function __editMSG(msg, frame) {
        var msgObj = mcService.StringToObj(msg.Msg);
        var idx = 0;

        if (msgObj.Idx && ((msgObj.Type == 0 || (msgObj.Type == 1 && msg.UIN == mcConst.UserInfo.UIN)) || !msgObj.Type)){
            idx = msgObj.Idx;
        }

        // var frameID = $rootScope.GetChatTypeID();
        var isConf  = !!msg.UID;
        var uin     = msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom;
        var message = document.getElementById(isConf ? `conf-${msg.UID}-${idx}` : `priv-${uin}-${idx}`);

        if (idx && message){
            var text   = message.getElementsByClassName("messageText")[0];
            var stItem = message.getElementsByClassName("statusItem")[0];
            var edit   = {
                text: msgObj.MsgNew.split(mcConst.CRLF),
                uin : msg.UINFrom !== void(0) ? msg.UINFrom : msg.UIN,
            };

            __textMessage(edit, msg);

            text.innerHTML   = edit.text;
            stItem.innerHTML = mcService.insertIco("fa-pencil grayLight2 ");

            if (msg.UINFrom != mcConst.UserInfo.UIN && stItem.className.indexOf("editRight") === -1){
                stItem.className += " editRight";
            }

            Self.scrollDown(frame, false, true);
        }
    }

    function __textMessage(total, msg) {
        for (var i = 0; i < total.text.length; i++){
            total.text[i] = total.text[i].replace(/</ig, "&lt;");
        }

        total.text.forEach(function (_txt, _line) {
            if (!msg.MsgType) {
                if (_txt.indexOf(mcConst.mcPara) === 0){
                    total.text[_line] = _txt.replace(mcConst.mcPara, "");
                } else
                if (_txt.indexOf(mcConst.mcParaCenter) === 0){
                    total.hasAlign    = true;
                    total.text[_line] = "<div class='byCenter messageTextP60'>" + _txt.replace(mcConst.mcParaCenter, "") + "</div>";
                } else
                if (_txt.indexOf(mcConst.mcParaRight) === 0){
                    total.hasAlign    = true;
                    total.text[_line] = "<div class='byRight messageTextP60'>" + _txt.replace(mcConst.mcParaRight, "") + "</div>";
                }
            }
        });

        total.text = total.text.join('<br/>');

        total.notifyText = total.text;

        total.text = mcService.replaceMcComponents(total.text, $rootScope.isWebClient, $rootScope);

        total.text = mcService.ReplaceURLs(total.text, { callback: function(_text, _link){
            var res = "";

            if (_link && _link.indexOf(mcConst.myChatLinkTitle) > 0){ // parse special mychat link
                _link = _link.split(mcConst.myChatLinkTitle);

                res   = _link[1];
                _link = _link[0];
            } else

            if (/(\.png|\.jpg|\.jpeg)$/.test(_link) && _link.indexOf(mcService.getLocalHostPath($rootScope.isWebClient)) === 0){
                res = '<br/><img class="thumbImage" src="' + _link + '">';
            } else

            if (_link && _link.indexOf(mcConst.ftpPersonal) === 0){
                _link = "";
                _text = __ftpPersonalLink(total);
            } else
            if (_link && _link.indexOf(mcConst.ftpPublic) === 0){
                _link = "";
                _text = __ftpPublicLink(total);
            }

            var desc = "";

            try {
                desc = decodeURI(res || _text);
            } catch (e) {
                desc = res || _text;
            }

            return _link ? '<a href="' + _link + '" title="' + _link + '" target="_blank">' + desc + '</a>' : _text;
        }});

        var symbol = "";
        var backupTxt = total.text;

        total.text = "";

        for ( var i = 0; i < backupTxt.length; i++ ){
            symbol = backupTxt.codePointAt(i);

            if (symbol >= 0x1F004 && symbol <= 0x1F9C0 && mcService.inArray(symbol, emojiList) >= 0) {
                total.text += "<img src='" + MC_RESOURCE.TextSource.smileys + "emoji/" + symbol.toString(16).toUpperCase() + ".png" + "' class='emojiSize'>"; // title='#{title}'

                i++;
            } else
                
            if (symbol >= 0x203C && symbol <= 0x3299 && mcService.inArray(symbol, emojiList) >= 0) {
                total.text += "<img src='" + MC_RESOURCE.TextSource.smileys + "emoji/" + symbol.toString(16).toUpperCase() + ".png" + "' class='emojiSize'>"; // title='#{title}'
            } else
                
            if (symbol === 0x00A9 || symbol === 0x00AE){
                total.text += "<img src='" + MC_RESOURCE.TextSource.smileys + "emoji/00" + symbol.toString(16).toUpperCase() + ".png" + "' class='emojiSize'>"; // title='#{title}'
            } else {
                total.text += backupTxt[i];
            }
        }
    }

    function __link(total, msg) {
        var link  = mcService.isString(msg.Msg) ? mcService.StringToObj(msg.Msg.replace(/\n/g, "")) : msg.Msg;
        var local = "";
        var desc  = "";

        if (msg.MsgType === mcConst._CMD_.msgType.LAN_LINK && link.Link /*&& link.Link.indexOf('\\\\') === 0*/) {
            if (msg.MsgType === mcConst._CMD_.msgType.LAN_LINK && link.Link.indexOf(link.Desc) !== -1){
                link.Desc = mcLang(669) + link.Link; // "669":"Сетевая ссылка:",
            } else {
                local = ": " + link.Link;
            }

            link.Link = link.Link.indexOf('\\\\\\\\') === 0 ? "local:" + link.Link.replace(/\\/g, "/") : "folder:" + link.Link;
        } else {
            link.Link = link.Link ? (link.Link.toLocaleLowerCase().indexOf("http") === -1 ? "http://" + link.Link : link.Link) : link;
        }

        try {
            desc = decodeURI(link.Desc);
        } catch (e) {
            desc = link.Desc;
        }
        
        total.text = mcService.myReplaceFormated(
            "<a href='#{link}' title='#{title}#{local}' target='_blank'>#{title}</a>",
            {
                link : link.Link,
                title: desc,
                local: local
            }
        );

        total.notifyText = mcLang(668); // "668":"Ссылка",
    }

    function __imageMessage(total, cText) {
        var resolutionW = total.info[4] && total.info[4] !== "-" ? Math.round(parseInt(total.info[4].split('x')[0])) : "auto";
        var resolutionH = total.info[4] && total.info[4] !== "-" ? Math.round(parseInt(total.info[4].split('x')[1])) : "auto";

        total.hash = total.info[0];

        if (total.info[4]){
            if (resolutionH >= resolutionW) {
                if (resolutionH > $rootScope.thumbsSize.y){
                    resolutionH = $rootScope.thumbsSize.y;
                }
                resolutionW = "auto";
            } else {
                if (resolutionW > $rootScope.thumbsSize.x){
                    resolutionW = $rootScope.thumbsSize.x;
                }
                resolutionH = "auto";
            }
        }

        total.text = mcService.myReplaceFormated(
            '<a ' +
                'id="link_#{idx}" ' +
                'href="noimage" '   +
                'target="_blank" '  +
                'style="display: inline-block; max-height: #{height}; max-width: #{width};"' +
            '><img id="img_#{idx}" class="noImageClick" src="#{loadImage}" dt="#{dt}" alt="" hash="#{hash}" msgIdx="#{msgIdx}" fileName="#{fileName}"></a>' +
            (cText ? "&nbsp;<i class='fa finger fa-clipboard copyTextBtn' txt='#{cText}'></i>" : ""),
            {
                idx      : total.info[0] + total.msgIdx,
                msgIdx   : total.msgIdx,
                hash     : total.hash,
                cText    : cText,
                fileName : total.info[1],
                loadImage: mcConst.imagesPath.loadImage,
                dt       : total.info[2],
                height   : resolutionH !== "auto" ? resolutionH + "px" : resolutionH,
                width    : resolutionW !== "auto" ? resolutionW + "px" : resolutionW
            }
        );

        /*Self.duManager*/loadImages.addImageToLoad(total.hash, total.msgIdx);

        total.loadThmb    = true;
        total.notifyText  = mcLang(570); // "570":"Опубликовано изображение",
    }

    function __imageMessageNew(msg, total, cText) {
        var info = mcService.StringToObj(msg.Msg);
        var resolutionW = info.Resolution && info.Resolution !== "-" ? Math.round(parseInt(info.Resolution.split('x')[0])) : "auto";
        var resolutionH = info.Resolution && info.Resolution !== "-" ? Math.round(parseInt(info.Resolution.split('x')[1])) : "auto";

        if (info.Resolution){
            if (resolutionH >= resolutionW) {
                if (resolutionH > $rootScope.thumbsSize.y){
                    resolutionH = $rootScope.thumbsSize.y;
                }
                resolutionW = "auto";
            } else {
                if (resolutionW > $rootScope.thumbsSize.x){
                    resolutionW = $rootScope.thumbsSize.x;
                }
                resolutionH = "auto";
            }
        }

        total.text = mcService.myReplaceFormated(
            '<a ' +
                'id="link_#{idx}" ' +
                'href="noimage" '   +
                'target="_blank" '  +
                'style="display: inline-block; max-height: #{height}; max-width: #{width};"' +
            '><img id="img_#{idx}" class="noImageClick" src="#{loadImage}" dt="#{dt}" alt="" hash="#{hash}" msgIdx="#{msgIdx}" fileName="#{fileName}"></a>' +
            (cText ? "&nbsp;<i class='fa finger fa-clipboard copyTextBtn' txt='#{cText}'></i>" : ""),
            {
                idx      : info.Hash + total.msgIdx,
                msgIdx   : total.msgIdx,
                hash     : info.Hash,
                cText    : cText,
                fileName : info.FileName,
                loadImage: mcConst.imagesPath.loadImage,
                dt       : info.UTCWriteTime,
                height   : resolutionH !== "auto" ? resolutionH + "px" : resolutionH,
                width    : resolutionW !== "auto" ? resolutionW + "px" : resolutionW
            }
        );

        /*Self.duManager*/loadImages.addImageToLoad(info.Hash, total.msgIdx);

        total.loadThmbNew = info;
        total.notifyText  = mcLang(570); // "570":"Опубликовано изображение",
    }

    function __fileMessage(total) {
        var filePath = total.info[1];
        var fileDT   = total.info[2];
        var fileSize = total.info[3];
        var fileName = mcService.extractFileName(filePath);

        total.hash = total.info[0];

        total.text =  mcService.myReplaceFormated(
            // icon
            '<i class="fa fa-file-o" aria-hidden="true"></i>' +

            // file name
            '<a id="link_mcService.extractFileName#{hash}#{id}" ' +
            ' hash    ="#{hash}" ' +
            ' filepath="#{filepath}" ' +
            ' filedt  ="#{filedt}" ' +
            ' filesize="#{filesize}" ' +
            ' href    ="#{href}" ' +
            ' nick    ="#{nick}" ' +
            ' uin     ="#{uin}" ' +
            ' target  ="_blank" ' +
            ' class   ="font120 fileLink openOrDownloadFile"' +
            '>#{file}</a>&nbsp;&nbsp;' +

            // download icon
            '<i class="fa fa-download finger downloadFile" aria-hidden="true" title="#{dTitle}"></i>&nbsp;' +

            // open folder icon
            ($rootScope.isWebClient ? '' : '<i class="fa fa-folder-open-o finger openInFolder" title="#{oTitle}" aria-hidden="true"></i>&nbsp;') + // openInFolder

            // file size
            '<span>(#{formatedSize})</span>',
            {
                id      : total.msgIdx,
                nick    : total.nick,
                uin     : total.uin,
                hash    : total.hash,
                time    : total.time,
                dTitle  : mcLang(573), // "573":"Скачать",
                oTitle  : mcLang(574), // "574":"Показать в папке",
                href    : mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasFiles + "/" + total.hash + "/" + encodeURI(fileName),
                file    : fileName,
                filepath: filePath,
                filedt  : fileDT,
                filesize: fileSize,
                formatedSize: mcService.formatFileSize(fileSize)
            }
        );

        total.notifyText = mcLang(571, fileName); // "571":"Файл: %s",
    }

    function __fileMessageNew(total, msg) {
        var info = mcService.StringToObj(msg.Msg);

        var filePath = info.FileName;
        var fileDT   = info.UTCWriteTime;
        var fileSize = info.Size;
        var fileName = mcService.extractFileName(filePath);
        var isMedia  = mcService.inArrayB(mcService.extractFileExtension(filePath), ['mpg', 'mp4', 'avi', 'mkv', 'webm']);


        total.hash = info.Hash;

        total.text =  mcService.myReplaceFormated(
            // icon
            '<i class="fa fa-file-o" aria-hidden="true"></i>' +

            // file name
            '<a id="link_#{hash}#{id}" ' +
            ' hash    ="#{hash}" ' +
            ' filepath="#{filepath}" ' +
            ' filedt  ="#{filedt}" ' +
            ' filesize="#{filesize}" ' +
            ' href    ="#{href}" ' +
            ' nick    ="#{nick}" ' +
            ' uin     ="#{uin}" ' +
            ' target  ="_blank" ' +
            ' class   ="font120 fileLink openOrDownloadFile"' +
            '>#{file}</a>&nbsp;&nbsp;' +

            // download icon
            '<i class="fa fa-download finger downloadFile" aria-hidden="true" title="#{dTitle}"></i>&nbsp;' +

            // open folder icon
            ($rootScope.isWebClient ? '' : '<i class="fa fa-folder-open-o finger openInFolder" title="#{oTitle}" aria-hidden="true"></i>&nbsp;') + // openInFolder

            (isMedia
                // play icon
                ? '<i class="fa fa-play-circle-o finger playMediaFile brown bolder font120" aria-hidden="true" title="Play media" link="#{href}"></i>&nbsp;'
                : ''
            ) +

            // file size
            '<span>(#{formatedSize})</span>',
            {
                id      : total.msgIdx,
                nick    : total.nick,
                uin     : total.uin,
                hash    : total.hash,
                time    : total.time,
                dTitle  : mcLang(573), // "573":"Скачать",
                oTitle  : mcLang(574), // "574":"Показать в папке",
                href    : mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasFiles + "/" + total.hash + "/" + encodeURI(fileName),
                file    : fileName,
                filepath: filePath,
                filedt  : fileDT,
                filesize: fileSize,
                formatedSize: mcService.formatFileSize(fileSize)
            }
        );

        total.notifyText = mcLang(571, fileName); // "571":"Файл: %s",
    }

    function __userNameMessage(total, noAvatar) {
        var isWebSup = total.nick.indexOf("(websup)") === 0;
        var rColor   = $rootScope.colorPalette[total.uin % $rootScope.colorPalette.length];
        var name     = total.nick.replace("(websup)", "<span class='bolder brown'>W</span>&nbsp;");
        var curFrame = $rootScope.GetChatTypeID();

        total.userName = mcService.myReplaceFormated(
            '<div id="__#{id}" class="grayBorderTop">' +
                '<div class="messageUserName lHeight20" uin="#{uin}">' +
                    (noAvatar
                        ? ''
                        : '<span class="circleAvatarSmall shadow2px boxShadow finger smallChatAvatar#{uin}" style="background-color: ##{rColor};" onclick="window._userActions.openUserProfile(#{uin})">#{icon}</span> '
                    ) +
                    '<span class="_messageUserName finger" uin="#{uin}" oncontextmenu="window._userActions.userMenu.apply(this)">#{nick}</span> ' +
                    '<span class="messageTime messageTimeHistory">#{badge}#{date}</span>' +
                '</div>' +
            '</div>',
            {
                rColor: rColor,
                nick  : name,
                icon  : isWebSup ? mcService.getTwoLettersName(total.nick.replace("(websup)", "")) : mcService.getTwoLettersName(name),
                uin   : total.uin,
                id    : total.msgIdx,
                date  : total.date,
                badge :
                    total.uin != mcConst.UserInfo.UIN &&
                    !total.History &&
                    (!mcConst.isWindowFocused || (!total.isConf && $rootScope.createUIN(total.uin) !== curFrame) || (total.isConf && $rootScope.createUID(total.UID) !== curFrame))
                        ? "<span class='msgCircle msgBadge-'" + total.uin + "></span> "
                        : ""
            }
        );

        if (!noAvatar) {
            if (loadAvatars.indexOf(total.uin) === -1) {
                loadAvatars.push(total.uin);

                if (!avatarTimer){
                    avatarTimer = setTimeout(function (){
                        var load = loadAvatars;

                        loadAvatars = [];
                        avatarTimer = null;

                        $rootScope.$broadcast(window._messages_.avatarsManager.showAvatars, [load, mcConst.avatars.SMALL]);
                    }, 1000);
                }
            }
        }
    }

    function __collectMessage(total) {
        // var canDelConfMsg = mcService.checkRights(mcConst._CMD_.RS.QAllowRemoveConfMessages);
        // var canDelPrivMsg = mcService.checkRights(mcConst._CMD_.RS.QAllowRemovePrivateMessages);
        var userName      = total.userName;
        var w100          = total.hasAlign ? "width100" : "messageTextP75";
        var mySeparator   = mcConst.ClientSettings.GeneralSeparateMyMessages && !total.userName ? "msgSeparator" : "";
        var uin           = total.uin;
        var Idx           = total.msgIdx;
        var postDate      = total.date;
        var postTime      = total.time;
        var timeTitle     = total.date + " " + total.time;
        var style         = "wordBreak: break-word";
        var text          = total.text;
        var time          = total.time;
        // var replyHint     = mcLang(725); // "725":"Ответить";
        // var delHint       = mcLang(519); // "519":"Удалить";
        // var editHint      = mcLang(761); // "761":"Редактировать сообщение",
        // var noTime        = "";
        var state         = total.uid ? "" : (total.notI ? "" : (__myMessages.IDMyMsgRead >= total.msgIdx ? "messageRead" : (__myMessages.IDMyMsgGot >= total.msgIdx ? "messageGot" : "messageSend" )));

        total.div.innerHTML =
            `${userName}` +
            `<div id='hsMsg-${Idx}' Idx='${Idx}' class='showOnHover msgWrap ${mySeparator}' uin='${uin}' postDate='${postDate}' postTime='${postTime}' 
                onmouseover='window._userActions.hideStatus.apply(this)' onmouseout='window._userActions.showStatus.apply(this)' style='${style}'>
                
                <span id='${Idx}-st-${uin}' class='${state} statusItem ${uin != mcConst.UserInfo.UIN || total.isConf ? 'editRight' : ''}'>${
                    total.history && total.mod === mcConst._CMD_.msgMods.MODIFIED 
                        ? mcService.insertIco('fa-pencil grayLight2') 
                        : ''
                }</span>

                <span class='webix_icon fa-ellipsis-h msgMenu fAwesome needShow' Idx='${Idx}' type='${total.msgType}' uin='${uin}' time='${total.dt}'></span>

                <span class='messageTime' title='${timeTitle}'> ${time}&nbsp;</span>
                <span class='messageText ${w100}'>${text}</span>
            </div>`
        ;

        /*
         <span class="msgMenuWrap">
         <span class='webix_icon fa-reply fAwesome replyMessage' Idx='${Idx}' title='${replyHint}'></span>
         ${((canDelConfMsg && total.isConf) || (!total.isConf && canDelPrivMsg)
         ? `<span class='webix_icon fa-trash-o fAwesome messageTrash ${noTime} deleteMessage' Idx='${Idx}' title='${delHint}'></span>`
         : ``)}
         <span class='webix_icon fa-pencil fAwesome' Idx='${Idx}' title='${editHint}'></span>
         </span>
         */

        // total.div.innerHTML = mcService.myReplaceFormated(
        //     "#{userName}" +
        //     "<div id='hsMsg-#{Idx}' Idx='#{Idx}' class='showOnHover msgWrap #{mySeparator}' uin='#{uin}' postDate='#{postDate}' postTime='#{postTime}' onmouseover='window._userActions.hideStatus.apply(this)' onmouseout='window._userActions.showStatus.apply(this)' style='#{style}'>" +
        //         "<span id='#{Idx}-st-#{uin}' class='#{state} statusItem'></span>" +
        //         "<span class='webix_icon fa-reply fAwesome needShow replyMessage' Idx='#{Idx}' title='#{replyHint}'></span>" +
        //         ((canDelConfMsg && total.isConf) || (!total.isConf && canDelPrivMsg)
        //             ? "<span class='webix_icon fa-trash-o fAwesome needShow messageTrash #{noTime} deleteMessage' Idx='#{Idx}' title='#{delHint}'></span>"
        //             : "") +
        //
        //         "<span class='messageTime' title='#{timeTitle}'> #{time}&nbsp;</span>" +
        //         "<span class='messageText #{w100}'>#{text}</span>" +
        //     "</div>",
        //     {
        //         userName : total.userName,
        //         w100     : total.hasAlign ? "width100" : "messageTextP75",
        //         mySeparator: mcConst.ClientSettings.GeneralSeparateMyMessages && !total.userName ? "msgSeparator" : "",
        //         uin      : total.uin,
        //         Idx      : total.msgIdx,
        //         postDate : total.date,
        //         postTime : total.time,
        //         timeTitle: total.date + " " + total.time,
        //         style    : "wordBreak: break-word",
        //         text     : total.text,
        //         // time     : $rootScope.isWebClient || mcConst.ClientSettings.EventsTimeStamp ? total.time : "",
        //         time     : total.time,
        //         delHint  : mcLang(519), // "519":"Удалить",
        //         replyHint: mcLang(725), // "725":"Ответить",
        //         // noTime   : $rootScope.isWebClient || mcConst.ClientSettings.EventsTimeStamp ? "" : "messageTrashNoTime",
        //         noTime   : "",
        //         state    : total.uid ? "" : (total.notI ? "" : (__myMessages.IDMyMsgRead >= total.msgIdx ? "messageRead" : (__myMessages.IDMyMsgGot >= total.msgIdx ? "messageGot" : "messageSend" )))
        //     }
        // );
    }

    function __callMessage(total, msg) {
        var callInfo    = mcService.StringToObj(msg.Msg);
        var isMyCall    = callInfo.UINFrom == mcConst.UserInfo.UIN;
        var time        = callInfo.Duration < 0 ? 0 : callInfo.Duration;
        var displayName = msg.Nick;

        switch (callInfo.State){
            case listCE.server_state_cancelled: // mediaEvents.stopCall:
                if (isMyCall){
                    total.text = mcLang(495, mcService.MyFormatTime(time, time < 3600)); // "495":"Исходящий звонок от меня, продолжительность %s",
                } else {
                    total.text = callInfo.Duration >= 0
                        ? mcLang(494, [displayName, mcService.MyFormatTime(time, time < 3600)]) // "494":"Входящий звонок от %s, продолжительность %s",
                        : mcLang(500); // "500":"Пропущен входящий звонок",
                }
            break;

            case listCE.server_state_rejected: //Self.callStates.reject:
                if (isMyCall){
                    total.text = mcLang(496); // "496":"Пользователь отказался принять ваш звонок",
                } else {
                    total.text = mcLang(500); // "500":"Пропущен входящий звонок",
                }
            break;

            case listCE.server_state_closed: //Self.callStates.close:
                if (isMyCall){
                    total.text = mcLang(495, mcService.MyFormatTime(time, time < 3600)); // "495":"Исходящий звонок от меня, продолжительность %s",
                } else {
                    total.text = mcLang(494, [displayName, mcService.MyFormatTime(time, time < 3600)]); // "494":"Входящий звонок от %s, продолжительность %s",
                }
            break;

            case listCE.server_state_error: //Self.callStates.error:
                total.text = mcService.Error(callInfo.ErrorNum, callInfo.ErrorDesc); // "499":"Во время звонка у вашего собеседника возникла ошибка",
            break;

            case listCE.server_state_busy: //Self.callStates.busy:
                total.text = mcLang(501); // "501":"Исходящий звонок от меня, занято.",
            break;

            case listCE.server_state_rejected_os_too_old: //Self.callStates.cancel:
                if (isMyCall){
                    total.text = mcLang(673); // "673":"Нельзя позвонить пользователю с Windows XP",
                } else {
                    total.text = mcLang(674); // "674":"Windows XP не поддерживает звонки в чате",
                }
            break;

            case listCE.server_state_cancelled_by_timeout:
                if (isMyCall){
                    total.text = mcLang(497); // "497":"Исходящий звонок от меня не принят.",
                } else {
                    total.text = mcLang(672); // "672":"Вы не приняли звонок",
                }
            break;

            case listCE.outgoing_call_remote_close:
                total.text = mcLang(495, mcService.MyFormatTime(time, time < 3600)); //
            break;

            case listCE.outgoing_call_rejected_user_not_in_contacts:
                if (isMyCall){
                    total.text = mcLang(728); // "728":"Вы отказались принять звонок от неизвестного контакта",
                } else {
                    total.text = mcLang(727); // "727":"Пользователь отказался принять звонок, вас нет в его контактах",
                }
            break;

            case listCE.incoming_call_rejected:
                total.text = mcLang(506); // "506":"Вы отказались принять звонок",
            break;

            default:
                if (mcService.isArray(total.text)){
                    total.text = total.text.join("");
                } else
                if (mcService.isObject(total.text)){
                    total.text = "Call";
                } else
                if (mcService.isObject(total.text)){

                }
        }

        total.notifyText = total.text;

        var _icon = mcService.insertIco("fa-" + (callInfo.Type == 0 ? "phone" : callInfo.Type == 1 ? "video-camera" : "desktop"));
        
        total.text = '<span class="messageText brown">' + _icon + total.text + '</span>';
    }

    function __ftpPublicLink(total) {
        total.text = "<a href='" + mcConst.ftpPublic + "' class='__ftpFolder__'>" + mcLang(652) + "</a>"; // "652":"Публичная FTP папка",;

        return total.text;
    }

    function __ftpPersonalLink(total) {
        total.text = "<a href='" + mcConst.ftpPersonal + total.uin + "' class='__ftpFolder__'>" + mcLang(653) + "</a>"; // "653":"Персональная FTP папка",

        return total.text;
    }

    function __ftpOpenLink(info) {
        var url  = "ftp://" + info.Where + ":" + info.Token + "@" + mcService.createHostPath(mcConst.UserInfo.ExternalIP || window.location.hostname, mcConst.UserInfo.PortFTP, true);

        mcService.openNewWindow(url, true);
    }

    function __BroadcastNotify(total, msg) {
       // "UIN":17614,
       // "TotalCount":1,
       // "ChangeID":61,
       // "ID":962,
       // "MsgBrief":"rtertert",
       // "ReadCount":1
        var info    = mcService.StringToObj(msg.Msg);
        var userInfo= $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [info.UIN]);
        var name    = "";

        if (userInfo.DisplayName === "noNameUser"){
            name = "<i id='broadcast-user-uin-" + info.UIN + "'>load name</i>";

            setTimeout(function (){
                $rootScope.$broadcast(window._messages_.usersSmallInfo.getInfoByUserUINs, [info.UIN, function (user){
                    if (user && user.length) {
                        var loadName = document.getElementById('broadcast-user-uin-' + info.UIN);
                        loadName.innerText = user[0].DisplayName;
                    }
                }, mcConst.UserInfo.hasInvite]);
            }, 100);
        } else {
            name = userInfo.DisplayName + " (UIN: " + info.UIN + ")";
        }

        total.text = mcLang(643, [info.ID, info.TotalCount, info.ReadCount, name]); // "643":"Оповещение №%s (%s/%s) получено: %s",
    }

    function __barcodeMessage(total) {
        var barData = mcService.StringToObj(total.text[0]);

        if (!mcService.isObject(barData)){
            total.text = "<br>---- Incorrect Barcode Data ----<br><br>";
            
            console.log("Bar code data: " + barData);

            return;
        }

        var type    = barData.Format.replace(/[_|-]/g, "").toLowerCase();

        if (barData && barData.Data) {
            if (type === "qrcode") {
                total.text = "<span id='qrcode_id_" + total.msgIdx + "'></span>";

                total.onAdd = function (){
                    new QRCode(document.getElementById("qrcode_id_" + total.msgIdx), {
                        text         : barData.Data,
                        width        : 200,
                        height       : 200,
                        colorDark    : "#131313",
                        colorLight   : "#ffffff",
                        correctLevel : QRCode.CorrectLevel.H
                    });
                };
            } else {
                if (type === "upca" || type === "ean13" || type === "ean8") {
                    if (type === "upca") {
                        type = "upcA"
                    }

                    var barcode = webix.ui({
                        view : "barcode",
                        type : type,
                        value: barData.Data
                    });

                    var barNode = barcode.getNode();

                    total.text = barNode.outerHTML.replace("<canvas ", "<img src=\"" + barNode.firstChild.toDataURL() + "\" ").replace("canvas>", "img>");

                    barcode.destructor();
                    barcode = null;
                } else {
                    total.text = "<img src='data:image/png;base64," + barData.Body + "' title='" + barData.Data + "'><br>" +
                                 "<span style='text-align: center; width: 100%; display: inline-block;'>" + barData.Data + "</span>";
                }
            }
        } else {
            total.text = "<br>---- Incorrect Barcode Data ----<br><br>";
        }
    }

    function __mychatGuest(total, msg) {
        // {"UID":-1,"ConfName":"",    "UserName":"Guest","dtUTC":"17.06.2022.20.59.59","Email":"gifer@nsoft-s.com","Type":1,
        // "Link":"https:\/\/mychat-server.com\/chat\/?invite=96C9D0DBFCFB60576E23A253D7D77A7E"}
        // {"UID":2, "ConfName":"work","UserName":"Guest","dtUTC":"17.06.2022.20.59.59","Email":"gifer@nsoft-s.com","Type":1,
        // "Link":"https:\/\/mychat-server.com\/chat\/?invite=869A93F452C324B27D16B0B9E27485D6"}
        var info = mcService.StringToObj(msg.Msg);

        total.text = mcLang(732) + mcService.ReplaceURLs(info.Link, { callback: function(_text, _link){ // "732":"Отправлено приглашение MyChat Guest, ссылка: ",
            var desc = "";

            try {
                desc = decodeURI(_link || _text);
            } catch (e) {
                desc = _link || _text;
            }

            return '<a href="' + _link + '" title="' + _link + '" target="_blank">' + desc + '</a>';
        }});
    }

    function __replyMsg(total, msg){
        // {
        //     "Original":{
        //         "UIN":17680,
        //         "Msg":"\u0448\u043e \u043f\u0438\u0441\u0443\u043b\u044c\u043a\u0430\u0442\u044c \u0443\u043c\u043e? \u0448\u043e \u043f\u043e\u0432\u0435\u0440\u043d\u0435\u043c \u043c\u0435\u043d\u0448\u0435\u043d\u0438 \u043a\u043e\u043b\u0438\u0441\u044c \u0442\u0430\u043c \u043d\u043e \u0445\u0437 \u043a\u043e\u043b\u0438?)",
        //         "Idx":19376,
        //         "MsgType":1,
        //         "dtUTC":"23.03.2021.12.02.29"
        //     },
        //     "Reply":{
        //         "Msg":"\u0446\u0438\u0442\u0430\u0442\u0430",
        //         "MsgType":1
        //     }
        // }
        var replyMsg    = mcService.StringToObj(msg.Msg);
        var originalMsg = mcService.StringToObj(replyMsg.Original);

        if (originalMsg){
            originalMsg.uin    = originalMsg.UIN;
            originalMsg.time   = originalMsg.dtUTC ? mcService.utcTime(originalMsg.dtUTC).split('.') : "";
            originalMsg.msgIdx = originalMsg.Idx;

            switch (originalMsg.MsgType){
                case mcConst._CMD_.msgType.LAN_LINK:
                case mcConst._CMD_.msgType.ANY_LINK:
                    originalMsg.Msg = mcService.StringToObj(originalMsg.Msg);

                    __link(originalMsg, originalMsg);
                break;

                case mcConst._CMD_.msgType.TEXT:
                    originalMsg.text = originalMsg.Msg.split(mcConst.CRLF);

                    __textMessage(originalMsg, originalMsg);
                break;

                case mcConst._CMD_.msgType.MEDIA_CALL:
                    __callMessage(originalMsg, originalMsg);
                break;

                case mcConst._CMD_.msgType.FTP_PUBLIC:
                    __ftpPublicLink(originalMsg);
                break;

                case mcConst._CMD_.msgType.FTP_PERSONAL:
                    __ftpPersonalLink(originalMsg);
                break;

                case mcConst._CMD_.msgType.EXCEL_NEW:
                case mcConst._CMD_.msgType.IMAGE_NEW:
                    originalMsg.Msg = mcService.StringToObj(originalMsg.Msg);
                    originalMsg.Msg.msgIdx = originalMsg.msgIdx = msg.Idx

                    __imageMessageNew(originalMsg, originalMsg);

                    total.loadThmbNew = originalMsg.Msg;
                break;

                case mcConst._CMD_.msgType.REPLY:
                    originalMsg.Msg = mcService.StringToObj(originalMsg.Msg);

                    var rpl = mcService.StringToObj(originalMsg.Msg.Original ? originalMsg.Msg.Original.Msg : originalMsg.Msg);

                    if (originalMsg.Msg.Original && !mcService.isObject(originalMsg.Msg.Original)){
                        originalMsg.Msg.Original = mcService.StringToObj(originalMsg.Msg.Original);

                        rpl = mcService.StringToObj(originalMsg.Msg.Original.Msg);
                    }

                    if (originalMsg.Msg.Original && originalMsg.Msg.Original.MsgType == mcConst._CMD_.msgType.OLD_SHIT) {
                        rpl = rpl.split(mcConst.newLine);
                        originalMsg.MsgType = mcConst._CMD_.msgType.OLD_SHIT;
                    }

                    originalMsg.Msg  = rpl.Reply ? rpl.Reply.Msg : rpl;

                    if (mcService.isObject(originalMsg.Msg)){
                        __fileMessageNew(originalMsg, originalMsg);
                    } else {
                        originalMsg.text = mcService.isArray(originalMsg.Msg) ? originalMsg.Msg : originalMsg.Msg.toString().split(mcConst.CRLF);

                        __textMessage(originalMsg, originalMsg);
                    }
                break;

                case mcConst._CMD_.msgType.FILE_NEW:
                    originalMsg.Msg = mcService.StringToObj(originalMsg.Msg);

                    __fileMessageNew(originalMsg, originalMsg);
                break;

                default:
                    originalMsg.text = "unknown reply type: " + originalMsg.MsgType;
            }

            if (originalMsg.time === '') {
                originalMsg.date = mcService.formatDate(new Date(), 'dd.mm.yyyy') + mcService.formatDate(new Date(), ' hh:nn:ss');
            } else {
                originalMsg.date = ((originalMsg.time.length >= 6) ? originalMsg.time[0] + "." + originalMsg.time[1] + "." + originalMsg.time[2] : originalMsg.time) + " " +
                    ((originalMsg.time.length >= 6) ? originalMsg.time[3] + ":" + originalMsg.time[4] + ":" + originalMsg.time[5] : originalMsg.time)
            }

            originalMsg.nick = originalMsg.Msg.DisplayName
                ? originalMsg.Msg.DisplayName
                : (originalMsg.UIN ? $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [originalMsg.UIN]).DisplayName : "noNameUser");

            __userNameMessage(originalMsg, true);

            total.text = replyMsg.Reply.Msg.split(mcConst.CRLF);

            for (var i = 0; i < total.text.length; i++){
                total.text[i] = total.text[i].replace(/</ig, "&lt;");
            }

            for (var i = 0; i < originalMsg.length; i++){
                originalMsg[i] = originalMsg[i].replace(/</ig, "&lt;");
            }

            total.text.forEach(function (_txt, _line) {
                if (!msg.MsgType) {
                    if (_txt.indexOf(mcConst.mcPara) === 0){
                        total.text[_line] = _txt.replace(mcConst.mcPara, "");
                    } else
                    if (_txt.indexOf(mcConst.mcParaCenter) === 0){
                        total.hasAlign    = true;
                        total.text[_line] = "<div class='byCenter messageTextP60'>" + _txt.replace(mcConst.mcParaCenter, "") + "</div>";
                    } else
                    if (_txt.indexOf(mcConst.mcParaRight) === 0){
                        total.hasAlign    = true;
                        total.text[_line] = "<div class='byRight messageTextP60'>" + _txt.replace(mcConst.mcParaRight, "") + "</div>";
                    }
                }
            });

            total.text  = total.text.join('<br/>');

            total.notifyText = total.text;

            total.text = mcService.replaceMcComponents(total.text, $rootScope.isWebClient, $rootScope);

            total.text = mcService.ReplaceURLs(total.text, { callback: function(_text, _link){
                var res = "";

                if (_link && _link.indexOf(mcConst.myChatLinkTitle) > 0){ // parse special mychat link
                    _link = _link.split(mcConst.myChatLinkTitle);

                    res   = _link[1];
                    _link = _link[0];
                } else

                if (/(\.png|\.jpg|\.jpeg)$/.test(_link) && _link.indexOf(mcService.getLocalHostPath($rootScope.isWebClient)) === 0){
                    res = '<br/><img class="thumbImage" src="' + _link + '">';
                } else

                if (_link && _link.indexOf(mcConst.ftpPersonal) === 0){
                    _link = "";
                    _text = __ftpPersonalLink(total);
                } else
                if (_link && _link.indexOf(mcConst.ftpPublic) === 0){
                    _link = "";
                    _text = __ftpPublicLink(total);
                }

                return _link ? '<a href="' + _link + '" title="' + _link + '" target="_blank">' + (res || _text) + '</a>' : _text;
            }});

            var symbol = "";
            var backupTxt = total.text; // where now is reply text

            total.text = "<div class='quoteMessage'>" + originalMsg.userName + originalMsg.text + "</div>";

            for ( var i = 0; i < backupTxt.length; i++ ){
                symbol = backupTxt.codePointAt(i);

                if (symbol >= 0x1F004 && symbol <= 0x1F9C0 && mcService.inArray(symbol, emojiList) >= 0) {
                    total.text += "<img src='" + MC_RESOURCE.TextSource.smileys + "emoji/" + symbol.toString(16).toUpperCase() + ".png" + "' class='emojiSize'>"; // title='#{title}'

                    i++;
                } else

                if (symbol >= 0x203C && symbol <= 0x3299 && mcService.inArray(symbol, emojiList) >= 0) {
                    total.text += "<img src='" + MC_RESOURCE.TextSource.smileys + "emoji/" + symbol.toString(16).toUpperCase() + ".png" + "' class='emojiSize'>"; // title='#{title}'
                } else

                if (symbol === 0x00A9 || symbol === 0x00AE){
                    total.text += "<img src='" + MC_RESOURCE.TextSource.smileys + "emoji/00" + symbol.toString(16).toUpperCase() + ".png" + "' class='emojiSize'>"; // title='#{title}'
                } else {
                    total.text += backupTxt[i];
                }
            }
        } else {
            console.log("Incorrect reply: " + JSON.stringify(msg));
        }
    }

    // ==== Draw ====================================

    function __codeSnippet(msg, frame){
        var total = {
            frame  : frame,
            target : Self.framesList.get(frame, true),
            div    : document.createElement('div'),
            // text   : msg.Text !== void(0) ? msg.Text.split(mcConst.mcPara) : msg.split(mcConst.mcPara),
            info   : mcService.StringToObj(msg.Msg),
            uin    : msg.UIN  !== void(0) ? msg.UIN : (msg.UINFrom !== void(0) ? msg.UINFrom : $rootScope.GetChatID()), //msg.UINFrom || msg.UIN;
            time   : msg.dtUTC ? msg.dtUTC.split('.') : (msg.dt ? msg.dt.split('.') : ""),
            notI   : true,
            msgIdx : msg.Idx,
            date   : "",
        };

        total.notI = total.uin != mcConst.UserInfo.UIN;
        total.isConf = msg.UID !== void(0);

        if (!total.time) {
            total.time = mcService.formatDate(new Date(), 'hh:nn');
            total.date = mcService.formatDate(new Date(), 'dd.mm.yyyy');
        } else {
            total.date = (total.time.length >= 6) ? total.time[0] + "." + total.time[1] + "." + total.time[2] : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
            total.time = (total.time.length >= 6) ? total.time[3] + ":" + total.time[4] /*+ ":" + total.time[5]*/ : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
        }

        if (msg.Idx) {
            total.div.setAttribute('Idx', msg.Idx);
            total.div.setAttribute('id' , total.isConf ? "conf-" + msg.UID + "-" + msg.Idx : "priv-" + (msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom) + "-" + msg.Idx);
        }

        total.div.className = (msg.Type ? "" : "brown ") + "framePaddingW5";
        total.div.style.wordBreak = "break-word";

        // if (msg.MsgType === mcConst._CMD_.msgType.CONF_INVITED) {
        total.nick = msg.Nick || msg.DisplayName || "cm_NoName";

        __userNameMessage(total);
        // }

        total.text = total.info.Msg;

        total.text = total.text.replace(/</ig, "&lt;");

        total.div.innerHTML = mcService.myReplaceFormated(
            '<div class="relative _messagesFontSize">#{name}' +
            // (($rootScope.isWebClient || mcConst.ClientSettings.EventsTimeStamp) && !noTimeStamp ? '<span class="messageTime" title="#{timeTitle}">#{time}</span> ' : "") +
            '<span class="messageTime" title="#{timeTitle}">#{time}&nbsp;</span> ' +
            '<pre class="messageText messageTextP60">#{text}</pre> ' +
            '</div>',
            {
                name     : total.userName ? total.userName : "",
                time     : total.time,
                timeTitle: total.date + " " + total.time,
                text     : total.text
            }
        );

        total.div.setAttribute('uin', total.uin);

        hljs.highlightElement(total.div.querySelector(".messageText"));

        RENDER_MSG.message(msg, total, RENDER_MSG.cost.normal);

        if (!msg.noNotify && !msg.History) {
            __showNotify(total.notI, "Code Snippet", "", total.isConf, total.isConf ? msg.UID : total.uin, msg.noBlink);
        }

    }

    function __addMSG(msg, frame){
        // window.___native_console.time("__addMSG" + rnd);

        var total = {
            frame      : frame,
            target     : Self.framesList.get(frame, true),
            div        : document.createElement('div'),
            text       : msg.MsgType ? msg.Msg.split(mcConst.CRLF) : msg.Msg.split(mcConst.newLine),
            uin        : msg.UINFrom !== void(0) ? msg.UINFrom : msg.UIN,
            nick       : msg.Nick,
            time       : msg.dt ? msg.dt.split('.') : '',
            dt         : msg.dt ? msg.dt.split('.') : [],
            date       : '',
            info       : msg.Msg.split(mcConst.CRLF),
            hash       : "",
            isConf     : msg.UID !== void(0),
            userName   : "",
            hasAlign   : false,
            loadThmb   : false,
            diffMinMsg : 10,
            notifyText : "",
            msgIdx     : msg.Idx,
            uid        : msg.UID,
            same       : false,
            msgType    : msg.MsgType,
            mod        : msg.Mod,
            history    : msg.History
        };
        var prevMessage = null;

        total.notI = total.uin != mcConst.UserInfo.UIN;

        if (msg.History) {
            total.same = RENDER_MSG.getLastHistoryMsg()
                ? RENDER_MSG.getLastHistoryMsg().uin
                : total.target.getByAttrFirst('uin');
        } else {
            total.same = RENDER_MSG.getLastMessage()
                ? RENDER_MSG.getLastMessage().uin
                : total.target.getByAttrLast('uin');

            prevMessage = RENDER_MSG.getLastMessage()
                ? RENDER_MSG.getLastMessage().div
                : total.target.getLastMsg();
        }

        total.same = total.uin == total.same;

        if (total.time === '') {
            total.time = mcService.formatDate(new Date(), 'hh:nn');
            total.date = mcService.formatDate(new Date(), 'dd.mm.yyyy');
        } else {
            total.date = (total.time.length >= 6) ? total.time[0] + "." + total.time[1] + "." + total.time[2] : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
            total.time = (total.time.length >= 6) ? total.time[3] + ":" + total.time[4] /*+ ":" + total.time[5]*/ : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
        }

        total.div.setAttribute('uin'     , total.uin);
        total.div.setAttribute('Idx'     , total.msgIdx);
        total.div.setAttribute('postDate', total.date);
        total.div.setAttribute('postTime', total.time);
        total.div.setAttribute('id'      , total.isConf ? "conf-" + total.uid + "-" + total.msgIdx : "priv-" + (msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom) + "-" + total.msgIdx);

        total.div.style.wordBreak = "break-word";
        total.div.className       = (total.notI ? "messageUser " : 'messageMy ') + " _messagesFontSize";

        total.prevDate    = prevMessage ? prevMessage.getAttribute('postDate') : null;
        total.prevTime    = prevMessage ? prevMessage.getAttribute('postTime') : null;
        total.timeDelta   = total.prevTime
            ?       moment(new Date(mcService.formatDate(total.date, "yyyy mm dd ")     + total.time))
              .diff(moment(new Date(mcService.formatDate(total.prevDate, "yyyy mm dd ") + total.prevTime)), 'minutes')
            : 100; //(total.diffMinMessages + 1);

        if (!total.same || msg.History || (!msg.History && prevMessage && ((total.prevDate !== total.date) || (total.prevDate === total.date && total.timeDelta > total.diffMinMsg)))){
            __userNameMessage(total);
        }

        switch (msg.MsgType){
            case mcConst._CMD_.msgType.OLD_SHIT:
            case mcConst._CMD_.msgType.INTEGRATION_API:
            case mcConst._CMD_.msgType.TEXT:
                __textMessage(total, msg);
            break;

            case mcConst._CMD_.msgType.LAN_LINK:
            case mcConst._CMD_.msgType.ANY_LINK:
                __link(total, msg);
            break;

            case mcConst._CMD_.msgType.BROADCAST_NOTIFY:
                __BroadcastNotify(total, msg);
            break;

            case mcConst._CMD_.msgType.MEDIA_CALL:
                __callMessage(total, msg);
            break;

            case mcConst._CMD_.msgType.FTP_PUBLIC:
                __ftpPublicLink(total);
            break;
            
            case mcConst._CMD_.msgType.FTP_PERSONAL:
                __ftpPersonalLink(total);
            break;

            case mcConst._CMD_.msgType.EXCEL_NEW:
            case mcConst._CMD_.msgType.IMAGE_NEW:
                var imageInfo = mcService.StringToObj(total.info[0]);
                
                __imageMessageNew(msg, total, mcService.isString(imageInfo)
                    ? total.info.length > 5
                        ? mcService.copy(total.info, 5).join(mcConst.CRLF)
                        : null
                    : imageInfo.Desc
                );
            break;

            case mcConst._CMD_.msgType.IMAGE:
            case mcConst._CMD_.msgType.CLIPBOARD_EXCEL:
                __imageMessage(total, total.info.length > 5
                    ? mcService.copy(total.info, 5).join(mcConst.CRLF)
                    : null
                );
            break;

            case mcConst._CMD_.msgType.FILE_NEW:
                __fileMessageNew(total, msg);
            break;

            case mcConst._CMD_.msgType.FILE:
                __fileMessage(total, msg);
            break;

            case mcConst._CMD_.msgType.BARCODE:
                __barcodeMessage(total);
            break;

            case mcConst._CMD_.msgType.REPLY:
                __replyMsg(total, msg);
            break;

            case mcConst._CMD_.msgType.MYCHAT_GUEST:
                __mychatGuest(total, msg);
            break;
        }

        __collectMessage(total);

        if ($rootScope.isWebClient) { // FTP click events
            var ftpFolder = total.div.getElementsByClassName("__ftpFolder__");

            for (var i = 0; i < ftpFolder.length; i++){
                var item = ftpFolder[i];

                item.onclick = function (e) {
                    e.preventDefault();

                    if (item.href.indexOf(mcConst.ftpPublic) === 0) {
                        $rootScope.SendCMDToServer([
                            mcConst._CMD_.cs_create_token,
                            mcConst.SessionID,

                            "ftp_public_token_" + mcConst.UserInfo.UIN,
                            "",

                            __ftpOpenLink
                        ]);
                    } else
                        
                    if (item.href.indexOf(mcConst.ftpPersonal) === 0) {
                        var uin = item.href.replace(mcConst.ftpPersonal, "").replace("/", "");

                        $rootScope.SendCMDToServer([
                            mcConst._CMD_.cs_create_token,
                            mcConst.SessionID,

                            uin == mcConst.UserInfo.UIN ? "ftp_personal_token_" + uin : "ftp_user_folder_read_only_token_" + uin + "_" + mcConst.UserInfo.UIN,
                            "",

                            __ftpOpenLink
                        ]);
                    }
                }
            }
        }

        total.History      = msg.History;
        total.UpSync       = msg.UpSync;
        total.noScrollDown = msg.noScrollDown;

        // window.___native_console.timeEnd("__addMSG" + rnd);

        RENDER_MSG.message(msg, total, RENDER_MSG.cost.normal);

        if (!msg.noUpdateCounter && !msg.noNotify && !(msg.History || msg.UpSync)) {
            __showNotify(total.notI, total.notifyText, total.nick, total.isConf, total.isConf ? total.uid : total.uin);
        }
    }

    function __addCustomMSG (msg, frame, noTimeStamp){
        if (mcService.isObject(msg)){
            msg.Text = msg.Text || msg.Msg;
        }

        var total = {
            frame  : frame,
            target : Self.framesList.get(frame, true),
            div    : document.createElement('div'),
            text   : msg.Text !== void(0) ? msg.Text.split(mcConst.mcPara) : msg.split(mcConst.mcPara),
            uin    : msg.UIN  !== void(0) ? msg.UIN : (msg.UINFrom !== void(0) ? msg.UINFrom : $rootScope.GetChatID()), //msg.UINFrom || msg.UIN;
            time   : msg.dtUTC ? msg.dtUTC.split('.') : (msg.dt ? msg.dt.split('.') : ""),
            notI   : true,
            msgIdx : msg.Idx,
            date   : "",
        };
        
        total.notI = total.uin != mcConst.UserInfo.UIN;
        total.isConf = msg.UID !== void(0);

        if (!total.time) {
            total.time = mcService.formatDate(new Date(), 'hh:nn');
            total.date = mcService.formatDate(new Date(), 'dd.mm.yyyy');
        } else {
            total.date = (total.time.length >= 6) ? total.time[0] + "." + total.time[1] + "." + total.time[2] : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
            total.time = (total.time.length >= 6) ? total.time[3] + ":" + total.time[4] /*+ ":" + total.time[5]*/ : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
        }

        if (mcService.trim(total.text[0]) === "") {
            total.text.shift();
        }

        for (var i = 0; i < total.text.length; i++){
            total.text[i] = total.text[i].replace(/</ig, "&lt;");
            total.text[i] = total.text[i].replace(/\u000b/ig, "");
        }

        total.text = mcService.ReplaceURLs(total.text.join('<br/>'), { callback: function(_text, _link){
            var res = "";

            if (/png|jpg|jpeg$/.test(_link)){
                res = '<br/><img class="thumbImage" src="' + _link + '">';
            }

            return _link ? '<a href="' + _link + '" title="' + _link + '" target="_blank">' + (res || _text) + '</a>' : _text;
        }});

        if (total.text.indexOf(mcConst.CRLF) >= 0) {
            total.text = total.text.replace(new RegExp(mcConst.CRLF, 'g'), "<br/>");
        }

        if (msg.inOnline){ // удаляем предыдущее сообщение, что пользователь опять в сети
            var prevDiv = document.getElementById(total.uin + "_inOnline");

            if (prevDiv) {
                clearTimeout(prevDiv.hideTimer);
                prevDiv.hideTimer = null;

                total.target.removeMsg(prevDiv.previousSibling);
                total.target.removeMsg(prevDiv);
            } else {
                total.div.id = total.uin + "_inOnline";
            }

            total.div.hideTimer =  setTimeout(function () {
                try {
                    total.target.removeMsg(total.div.previousSibling);
                    total.target.removeMsg(total.div);
                } catch (e) {}
            }, 25000);
        }

        total.div.className = (msg.Type ? "" : "brown ") + "framePaddingW5";
        total.div.style.wordBreak = "break-word";

        if (msg.MsgType === mcConst._CMD_.msgType.CONF_INVITED) {
            total.nick = msg.Nick || msg.DisplayName || "cm_NoName";
            
            __userNameMessage(total);
        }

        total.div.innerHTML = mcService.myReplaceFormated(
            '<div class="relative _messagesFontSize">#{name}' +
            // (($rootScope.isWebClient || mcConst.ClientSettings.EventsTimeStamp) && !noTimeStamp ? '<span class="messageTime" title="#{timeTitle}">#{time}</span> ' : "") +
            (!noTimeStamp ? '<span class="messageTime" title="#{timeTitle}">#{time}</span> ' : "") +
                '<span class="messageText messageTextP60"><i class="fa fa-bullhorn" aria-hidden="true"></i> #{text}</span> ' +
            '</div>',
            {
                name     : total.userName ? total.userName : "",
                time     : total.time,
                timeTitle: total.date + " " + total.time,
                text     : total.text
            }
        );

        total.div.setAttribute('uin', total.uin);

        if (msg.Idx) {
            total.div.setAttribute('Idx', msg.Idx);
            total.div.setAttribute('id' , total.isConf ? "conf-" + msg.UID + "-" + msg.Idx : "priv-" + (msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom) + "-" + msg.Idx);
        }

        RENDER_MSG.message(msg, total, RENDER_MSG.cost.custom);

        if (!msg.noNotify && !msg.History) {
            __showNotify(total.notI, total.text, "", total.isConf, total.isConf ? msg.UID : total.uin, msg.noBlink);
        }
    }

    function __addTechMSG (msg, frame, noTimeStamp){
        if (mcService.isObject(msg)){
            msg.Text = msg.Text || msg.Msg;
        }

        var total = {
            frame  : frame,
            target : Self.framesList.get(frame, true),
            div    : document.createElement('div'),
            text   : msg.Text !== void(0) ? msg.Text.split(mcConst.mcPara) : msg.split(mcConst.mcPara),
            uin    : msg.UIN, //  !== void(0) ? msg.UIN : (msg.UINFrom !== void(0) ? msg.UINFrom : $rootScope.GetChatID()), //msg.UINFrom || msg.UIN;
            time   : msg.dtUTC ? msg.dtUTC.split('.') : (msg.dt ? msg.dt.split('.') : ""),
            notI   : true,
            msgIdx : "techMSG-" + mcService.RandomHash(4), //msg.Idx,
            date   : "",
        };

        total.notI   = total.uin != mcConst.UserInfo.UIN;
        total.isConf = msg.Place == 2;

        if (!total.time) {
            total.time = mcService.formatDate(new Date(), 'hh:nn');
            total.date = mcService.formatDate(new Date(), 'dd.mm.yyyy');
        } else {
            total.date = total.time.length >= 6 ? total.time[0] + "." + total.time[1] + "." + total.time[2] : total.time;
            total.time = total.time.length >= 6 ? total.time[3] + ":" + total.time[4]                       : total.time;
        }

        if (mcService.trim(total.text[0]) === "") {
            total.text.shift();
        }

        for (var i = 0; i < total.text.length; i++){
            total.text[i] = total.text[i].replace(/</ig, "&lt;");
            total.text[i] = total.text[i].replace(/\u000b/ig, "");
        }

        total.text = mcService.ReplaceURLs(total.text.join('<br/>'), { callback: function(_text, _link){
            var res = "";

            if (/png|jpg|jpeg$/.test(_link)){
                res = '<br/><img class="thumbImage" src="' + _link + '">';
            }

            return _link ? '<a href="' + _link + '" title="' + _link + '" target="_blank">' + (res || _text) + '</a>' : _text;
        }});

        if (total.text.indexOf(mcConst.CRLF) >= 0) {
            total.text = total.text.replace(new RegExp(mcConst.CRLF, 'g'), "<br/>");
        }

        if (msg.inOnline){ // удаляем предыдущее сообщение, что пользователь опять в сети
            var prevDiv = document.getElementById(total.uin + "_inOnline");

            if (prevDiv) {
                clearTimeout(prevDiv.hideTimer);
                prevDiv.hideTimer = null;

                total.target.removeMsg(prevDiv.previousSibling);
                total.target.removeMsg(prevDiv);
            } else {
                total.div.id = total.uin + "_inOnline";
            }

            total.div.hideTimer =  setTimeout(function () {
                try {
                    total.target.removeMsg(total.div.previousSibling);
                    total.target.removeMsg(total.div);
                } catch (e) {}
            }, 25000);
        }

        total.div.className = (msg.Type ? "" : "brown ") + "framePaddingW5";
        total.div.style.wordBreak = "break-word";

        // if (msg.Place == 2) {
        //     total.nick = msg.Nick || msg.DisplayName || "cm_NoName";
        //
        //     __userNameMessage(total);
        // }

        total.div.innerHTML = mcService.myReplaceFormated(
            '<div class="relative _messagesFontSize borderMSG">#{name}' +
            (!noTimeStamp ? '<span class="messageTime" title="#{timeTitle}">#{time}</span> ' : "") +
                '<span class="messageText messageTextP60"><i class="fa fa-bullhorn" aria-hidden="true"></i> #{text}</span> ' +
            '</div>',
            {
                name     : total.userName ? total.userName : "",
                time     : total.time,
                timeTitle: total.date + " " + total.time,
                text     : total.text
            }
        );

        total.div.setAttribute('uin', total.uin);

        if (msg.Idx) {
            total.div.setAttribute('Idx', msg.Idx);
            total.div.setAttribute('id' , total.isConf ? "conf-" + msg.UID + "-" + msg.Idx : "priv-" + (msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom) + "-" + msg.Idx);
        }

        RENDER_MSG.message(msg, total, RENDER_MSG.cost.custom);


        if (!msg.noNotify && !msg.History) {
            __showNotify(total.notI, total.text, "", total.isConf, total.isConf ? msg.UID : total.uin, msg.noBlink);
        }
    }

    function __addRedirectMSG (msg, frame, noTimeStamp){
        var total = {
            frame  : frame,
            target : Self.framesList.get(frame, true),
            div    : document.createElement('div'),
            text   : "",
            uin    : msg.UIN !== void(0) ? msg.UIN : (msg.UINFrom !== void(0) ? msg.UINFrom : $rootScope.GetChatID()), //msg.UINFrom || msg.UIN;
            // time   : mcService.formatDate(msg.dt || (new Date()), 'hh:nn:ss'),
            time   : msg.dtUTC ? msg.dtUTC.split('.') : (msg.dt ? msg.dt.split('.') : ""),
            rdData : mcService.StringToObj(msg.Msg)
        };

        total.notI   = total.uin != mcConst.UserInfo.UIN;
        total.isConf = msg.UID !== void(0);

        if (!total.time) {
            total.time = mcService.formatDate(new Date(), 'hh:nn');
            total.date = mcService.formatDate(new Date(), 'dd.mm.yyyy');
        } else {
            total.date = (total.time.length >= 6) ? total.time[0] + "." + total.time[1] + "." + total.time[2] : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
            total.time = (total.time.length >= 6) ? total.time[3] + ":" + total.time[4] /*+ ":" + total.time[5]*/ : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
        }

        if (total.rdData.UIN == mcConst.UserInfo.UIN){
            total.text = mcLang(615, [total.rdData.DisplayNameTo, total.rdData.DisplayNameFrom]); // "615":"Вы перенаправлены к пользователю <span class='blue'>%s</span>",
        } else
        if (total.rdData.UINFrom == mcConst.UserInfo.UIN){
            total.text = mcLang(616, [total.rdData.DisplayName, total.rdData.DisplayNameTo]); // "616":"Вы перенаправили пользователя <span class='blue'>%s</span> к <span class='blue'>%s</span>",
        } else
        if (total.rdData.UINTo == mcConst.UserInfo.UIN){
            total.text = mcLang(617, [total.rdData.DisplayName, total.rdData.DisplayNameFrom]); // "617":"Пользователь <span class='blue'>%s</span> перенаправлен к вам от <span class='blue'>%s</span>",
        }

        total.div.className = (msg.Type ? "" : "brown ") + "framePaddingW5";
        total.div.style.wordBreak = "break-word";

        // total.div.innerHTML = '<div class="borderTop relative _messagesFontSize">' +
        //     (($rootScope.isWebClient || mcConst.ClientSettings.EventsTimeStamp) && !noTimeStamp ? '<span class="messageTime">' + total.time + '</span> ' : "") +
        //        '<span class="messageText messageTextP60"><i class="fa fa-bullhorn" aria-hidden="true"></i> ' + total.text + '</span> ' +
        //     '</div>';
        total.div.innerHTML = mcService.myReplaceFormated(
            '<div class="borderTop relative _messagesFontSize">' +
            // (($rootScope.isWebClient || mcConst.ClientSettings.EventsTimeStamp) && !noTimeStamp ? '<span class="messageTime" title="#{timeTitle}">#{time}</span> ' : "") +
            (!noTimeStamp ? '<span class="messageTime" title="#{timeTitle}">#{time}</span> ' : "") +
                '<span class="messageText messageTextP60"><i class="fa fa-bullhorn" aria-hidden="true"></i> #{text}</span> ' +
            '</div>',
            {
                time     : total.time,
                timeTitle: total.date + " " + total.time,
                text     : total.text
            }
        );

        total.div.setAttribute('uin', total.uin);

        if (msg.Idx) {
            total.div.setAttribute('Idx', msg.Idx);
            total.div.setAttribute('id' , total.isConf ? "conf-" + msg.UID + "-" + msg.Idx : "priv-" + (msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom) + "-" + msg.Idx);

        }

        RENDER_MSG.message(msg, total, RENDER_MSG.cost.redirect);
        
        !msg.History && __showNotify(total.notI, total.text, "", total.isConf, total.isConf ? msg.UID : total.uin);
    }

    function __addKanbanMSG (msg, frame){
        var total = {
            frame : frame,
            target: Self.framesList.get(frame, true),
            div   : document.createElement('div'),
            kMSG  : JSON.parse(msg.Msg),
            uin   : msg.UIN !== void(0) ? msg.UIN : (msg.UINFrom !== void(0) ? msg.UINFrom : $rootScope.GetChatID()), //msg.UINFrom || msg.UIN;
            // time  : msg.dt ? mcService.formatDate(msg.dt, 'dd.mm.yyyy hh:nn:ss') : mcService.formatDate(new Date(), 'hh:nn:ss'),
            time  : msg.dtUTC ? msg.dtUTC.split('.') : (msg.dt ? msg.dt.split('.') : ""),
            text  : "",
            notifyText: null
        };

        if (!total.time) {
            total.time = mcService.formatDate(new Date(), 'hh:nn');
            total.date = mcService.formatDate(new Date(), 'dd.mm.yyyy');
        } else {
            total.date = (total.time.length >= 6) ? total.time[0] + "." + total.time[1] + "." + total.time[2] : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
            total.time = (total.time.length >= 6) ? total.time[3] + ":" + total.time[4] /*+ ":" + total.time[5]*/ : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
        }

        if (total.kMSG.TaskName) {
            switch (total.kMSG.What){
                case mcConst.KanbanEvents.TASK_ADDED: case mcConst.KanbanEvents.TASK_PERFORMER_CHANGED:
                    total.text = mcLang(543, [ // "543":"<a href='%s' target='_blank'>Новое задание</a> от %s<br>Задание:<br>%s<br>Описание:<br>%s",
                        mcService.createKanbanLink(total.kMSG.ProjectID, total.kMSG.TaskID),
                        total.kMSG.DisplayName,
                        __parseLink(total.kMSG.TaskName),
                        __parseLink(total.kMSG.Description)
                    ]);

                    total.notifyText = "KANBAN Task:<br>" + total.kMSG.TaskName;
                break;

                case mcConst.KanbanEvents.COMMENT_ADDED:
                    total.text = mcLang(544, [ // "544":"<a href='%s' target='_blank'>Новый комментарий</a> от %s<br>Задание:<br>%s<br>Комментарий:<br>%s",
                        mcService.createKanbanLink(total.kMSG.ProjectID, total.kMSG.TaskID, total.kMSG.CommentID),
                        total.kMSG.DisplayName,
                        __parseLink(total.kMSG.TaskName),
                        __parseLink(total.kMSG.Comment)
                    ]);

                    total.notifyText = "KANBAN Comment:<br>" + total.kMSG.Comment;
                break;

                case mcConst.KanbanEvents.TASK_MOVED:
                    total.text = mcLang(545, [ // "545":"<a href='%s' target='_blank'>Задание перемещено</a> пользователем %s<br>Задание:<br>%s<br>Откуда и куда:<br>%s",
                        mcService.createKanbanLink(total.kMSG.ProjectID, total.kMSG.TaskID),
                        total.kMSG.DisplayName,
                        __parseLink(total.kMSG.TaskName),
                        total.kMSG.OldStageName + " &gt; " + total.kMSG.StageName
                    ]);

                    total.notifyText = "KANBAN Task:<br>" + total.kMSG.TaskName;
                break;

                case mcConst.KanbanEvents.TASK_CHANGED:
                    total.text = mcLang(546, [ // "546":"<a href='%s' target='_blank'>Задание изменено</a> пользователем %s<br>Задание:<br>%s<br>Описание:<br>%s",
                        mcService.createKanbanLink(total.kMSG.ProjectID, total.kMSG.TaskID),
                        total.kMSG.DisplayName,
                        __parseLink(total.kMSG.TaskName),
                        __parseLink(total.kMSG.Description)
                    ]);

                    total.notifyText = "KANBAN Task:<br>" + total.kMSG.TaskName;
                break;

                case mcConst.KanbanEvents.TASK_DELETED:
                    total.text = mcLang(547, [ // "547":"<a href='%s' target='_blank'>Задание удалено</a> пользователем %s<br>Задание:<br>%s<br>Описание:<br>%s<br>Проект:<br>%s",
                        mcService.createKanbanLink(total.kMSG.ProjectID, total.kMSG.TaskID),
                        total.kMSG.DisplayName,
                        __parseLink(total.kMSG.TaskName),
                        __parseLink(total.kMSG.Description),
                        total.kMSG.ProjectName
                    ]);

                    total.notifyText = "KANBAN Task:<br>" + total.kMSG.TaskName;
                break;

                case mcConst.KanbanEvents.PROJECT_CLOSED:
                    total.text = mcLang(548, [ // "548":"<a href='%s' target='_blank'>Проект закрыт</a> пользователем %s<br>Проект:<br>%s",
                        mcService.createKanbanLink(total.kMSG.ProjectID, total.kMSG.TaskID),
                        total.kMSG.DisplayName,
                        total.kMSG.ProjectName
                    ]);

                    total.notifyText = "KANBAN Project:<br>" + total.kMSG.ProjectName;
                break;
            }

            total.div.className = "framePaddingW5";
            total.div.style.wordBreak = "break-word";

            total.div.innerHTML = '<div class="borderTop marginTop10 _messagesFontSize"><i class="fa fa-bullhorn" aria-hidden="true"></i>' +
                '<span class="messageKanbanTime toRight lineHeight24" title="' + (total.date + " " + total.time) + '">' + total.time + '</span> ' +
                '<span class="messageKanban">' + total.text + '</span></div>';

            total.div.setAttribute('uin', total.uin);

            if (msg.Idx) {
                total.div.setAttribute('Idx', msg.Idx);
                total.div.setAttribute('id' , total.isConf ? "conf-" + msg.UID + "-" + msg.Idx : "priv-" + (msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom) + "-" + msg.Idx);
            }

            RENDER_MSG.message(msg, total, RENDER_MSG.cost.kanban);
            
            !msg.History && __showNotify(true, total.notifyText, total.kMSG.DisplayName, false, total.uin);
        }
    }

    function __addForumMSG (msg, frame){
        var total = {
            frame : frame,
            target: Self.framesList.get(frame, true),
            div   : document.createElement('div'),
            fMSG  : JSON.parse(msg.Msg),
            uin   : msg.UIN !== void(0) ? msg.UIN : (msg.UINFrom !== void(0) ? msg.UINFrom : $rootScope.GetChatID()), //msg.UINFrom || msg.UIN;
            // time  : msg.dt ? mcService.formatDate(msg.dt, 'dd.mm.yyyy hh:nn:ss') : mcService.formatDate(new Date(), 'hh:nn:ss'),
            time  : msg.dtUTC ? mcService.utcTime(msg.dtUTC).split('.') : (msg.dt ? msg.dt.split('.') : ""),
            text  : "",
            notifyText: null
        };

        if (!total.time) {
            total.time = mcService.formatDate(new Date(), 'hh:nn');
            total.date = mcService.formatDate(new Date(), 'dd.mm.yyyy');
        } else {
            total.date = (total.time.length >= 6) ? total.time[0] + "." + total.time[1] + "." + total.time[2] : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
            total.time = (total.time.length >= 6) ? total.time[3] + ":" + total.time[4] /*+ ":" + total.time[5]*/ : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
        }

        var CREATED = 1;
        var CHANGED = 2;

        if (total.fMSG.Topic) {
            switch (total.fMSG.What) {
                case CREATED:
                    total.text = mcService.myReplaceFormated(
                        "<a href='#{link}' target='_blank'>#{what}</a><br><span class='brown'>#{topicTitle}</span>: #{topic}<br><span class='brown'>#{userTitle}</span>: #{user}",
                        {
                            link      : mcService.createForumLink(total.fMSG.Path, total.fMSG.PostID),
                            what      : mcLang(678),           // "678":"Сообщение на форуме",
                            topicTitle: mcLang(680),           // "680":"Тема",
                            topic     : total.fMSG.Topic,      //
                            userTitle : mcLang(679),           // "679":"Автор",
                            user      : total.fMSG.DisplayName //
                        }
                    );
                break;

                case CHANGED:
                    total.text = mcService.myReplaceFormated(
                        "<a href='#{link}' target='_blank'>#{what}</span><br><span class='brown'>#{topicTitle}</span>: #{topic}<br><span class='brown'>#{userTitle}</span>: #{user}",
                        {
                            link      : mcService.createForumLink(total.fMSG.Path, total.fMSG.PostID),
                            what      : mcLang(681),           // "678":"Сообщение на форуме",
                            topicTitle: mcLang(680),           // "680":"Тема",
                            topic     : total.fMSG.Topic,      //
                            userTitle : mcLang(679),           // "679":"Автор",
                            user      : total.fMSG.DisplayName //
                        }
                    );
                break;
            }

            total.div.className = "framePaddingW5";
            total.div.style.wordBreak = "break-word";

            total.div.innerHTML = '<div class="borderTop marginTop10 _messagesFontSize"><i class="fa fa-bullhorn" aria-hidden="true"></i>' +
                '<span class="messageKanbanTime toRight lineHeight24" title="' + (total.date + " " + total.time) + '">' + total.time + '</span> ' +
                '<span class="messageKanban">' + total.text + '</span></div>';

            total.div.setAttribute('uin', total.uin);

            if (msg.Idx) {
                total.div.setAttribute('Idx', msg.Idx);
                total.div.setAttribute('id' , total.isConf ? "conf-" + msg.UID + "-" + msg.Idx : "priv-" + (msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom) + "-" + msg.Idx);
            }

            RENDER_MSG.message(msg, total, RENDER_MSG.cost.forum);

            !msg.History && __showNotify(true, total.notifyText, total.fMSG.DisplayName, false, total.uin);
        }
    }

    function __addReceivedFilesMSG(msg, frame) {
        var total = {
            frame    : frame,
            target   : Self.framesList.get(frame, true),
            div      : document.createElement('div'),
            uin      : msg.UIN,
            // time     : mcService.formatDate(new Date(), 'hh:nn:ss'),
            time     : msg.dtUTC ? msg.dtUTC.split('.') : (msg.dt ? msg.dt.split('.') : ""),
            text     : "",
            fileList : msg.FilesList,
            fileCount: msg.Count,
            fileSize : msg.Size,
            filePath : msg.Path,
            count    : 0,
            more     : false,
            topText  : 597
        };

        if (!total.time) {
            total.time = mcService.formatDate(new Date(), 'hh:nn');
            total.date = mcService.formatDate(new Date(), 'dd.mm.yyyy');
        } else {
            total.date = (total.time.length >= 6) ? total.time[0] + "." + total.time[1] + "." + total.time[2] : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
            total.time = (total.time.length >= 6) ? total.time[3] + ":" + total.time[4] /*+ ":" + total.time[5]*/ : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
        }

        switch (msg.MsgType){
            case mcConst._CMD_.msgType.SHOW_RECEIVED_FILES: total.topText = 597; break; // "597":"Принято %s файлов, общий рамер: %
            case mcConst._CMD_.msgType.SHOW_UPLOADED_FILES: total.topText = 599; break; // "599":"ОТправлено %s файлов, общий рамер: %s",
        }

        var path = "";

        if (mcService.isString(total.filePath)){
            path = total.filePath.split(',')[0];
        } else
        if (mcService.isArray(total.filePath)) {
            path = total.filePath[0];
        } else {
            path = total.filePath;
        }

        total.text = mcService.myReplaceFormated(
            "<span class='finger bolder #{addClass} openFolder' filepath='#{path}' title='#{oTitle}'>#{msg}</span>",
            {
                path    : path,
                msg     : mcLang(total.topText, [total.fileCount, mcService.formatFileSize(total.fileSize)]),
                oTitle  : mcLang(574), // "574":"Показать в папке",
                addClass: "fileLink"
            }
        );

        total.fileList.forEach(function (_file) {
            // if ((_file.received || _file.sended) && total.count < 10){
            if (total.count < 10){
                total.text += mcService.myReplaceFormated(
                    "<div><span class='brown'>#{name}</span> (#{size})</div>",
                    {
                        name: _file.filePath || _file.fileName,
                        size: mcService.formatFileSize(_file.size)
                    }
                );
            } else {
                total.more = true;
            }

            total.count ++;
        });

        if (total.count > 10) {
            total.text += "<div>" + mcLang(598, total.count - 10) + "</div>"; // "598":"... еще %s файлов",
        }

        total.div.className = "framePaddingW5";
        total.div.style.wordBreak = "break-word";

        total.div.innerHTML = mcService.myReplaceFormated(
            '<div class="borderTop _messagesFontSize">' +
                '<span filepath="#{path}" class="messageText"><i class="fa fa-folder-open-o" aria-hidden="true"></i> #{text}</span>' +
                '<span class="messageTime" title="#{dateTime}">#{time}</span> ' +
            '</div>',
            {
                // time   : $rootScope.isWebClient || mcConst.ClientSettings.EventsTimeStamp ? total.time : "",
                time   : total.time,
                text   : total.text,
                path   : total.filePath,
                dateTime:total.date + " " + total.time,
            }
        );

        total.div.setAttribute('uin', total.uin);
        total.div.setAttribute('id' , total.isConf ? "conf-" + msg.UID + "-" + msg.Idx : "priv-" + total.uin + "-" + msg.Idx);

        RENDER_MSG.message(msg, total, RENDER_MSG.cost.files);
    }

    function __addBanUser(msg, frame){
        if (mcService.isObject(msg)){
            msg.Text = msg.Text || msg.Msg;
        }

        var total = {
            frame  : frame,
            target : Self.framesList.get(frame, true),
            div    : document.createElement('div'),
            text   : msg.Text !== void(0) ? msg.Text.split(mcConst.mcPara) : msg.split(mcConst.mcPara),
            uin    : msg.UIN  !== void(0) ? msg.UIN : (msg.UINFrom !== void(0) ? msg.UINFrom : $rootScope.GetChatID()), //msg.UINFrom || msg.UIN;
            time   : msg.dtUTC ? msg.dtUTC.split('.') : (msg.dt ? msg.dt.split('.') : ""),
            notI   : true,
            msgIdx : msg.Idx,
            date   : "",
        };

        total.notI = total.uin != mcConst.UserInfo.UIN;
        total.isConf = msg.UID !== void(0);

        if (total.time === '') {
            total.time = mcService.formatDate(new Date(), 'hh:nn');
            total.date = mcService.formatDate(new Date(), 'dd.mm.yyyy');
        } else {
            total.date = (total.time.length >= 6) ? total.time[0] + "." + total.time[1] + "." + total.time[2] : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
            total.time = (total.time.length >= 6) ? total.time[3] + ":" + total.time[4] /*+ ":" + total.time[5]*/ : total.time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
        }

        total.div.innerHTML = mcService.myReplaceFormated(
            '<div class="relative _messagesFontSize">#{name}' +
            // (($rootScope.isWebClient || mcConst.ClientSettings.EventsTimeStamp) && !noTimeStamp ? '<span class="messageTime" title="#{timeTitle}">#{time}</span> ' : "") +
            "<span class='messageTime' title='#{timeTitle}'> #{time}&nbsp;</span>" +
            '<span class="messageText messageTextP60"><i class="fa fa-bullhorn" aria-hidden="true"></i> #{text}</span> ' +
            '</div>',
            {
                name     : total.userName ? total.userName : "",
                time     : total.time,
                timeTitle: total.date + " " + total.time,
                text     : total.text
            }
        );

        total.div.setAttribute('uin', total.uin);

        if (msg.Idx) {
            total.div.setAttribute('Idx', msg.Idx);
            total.div.setAttribute('id' , total.isConf ? "conf-" + msg.UID + "-" + msg.Idx : "priv-" + (msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom) + "-" + msg.Idx);
        }

        RENDER_MSG.message(msg, total, RENDER_MSG.cost.custom);

        // if (!msg.noNotify && !msg.History) {
        //     __showNotify(total.notI, total.text, "", total.isConf, total.isConf ? msg.UID : total.uin, msg.noBlink);
        // }
    }

    function __mediaConfMSG(msg, frame, _data){
        // if (mcService.isObject(msg)){
        //     msg.Text = msg.Text || msg.Msg;
        // }

        var total = {
            frame  : frame,
            target : Self.framesList.get(frame, true),
            div    : document.createElement('div'),
            // text   : msg.Text !== void(0) ? msg.Text.split(mcConst.mcPara) : msg.split(mcConst.mcPara),
            uin    : msg.UIN  !== void(0) ? msg.UIN : (msg.UINFrom !== void(0) ? msg.UINFrom : $rootScope.GetChatID()), //msg.UINFrom || msg.UIN;
            time   : msg.dtUTC ? msg.dtUTC.split('.') : (msg.dt ? msg.dt.split('.') : ""),
            notI   : true,
            msgIdx : msg.Idx,
            date   : "",
        };

        total.notI = total.uin != mcConst.UserInfo.UIN;
        total.isConf = msg.UID !== void(0);

        if (total.time === '') {
            total.time = mcService.formatDate(new Date(), 'hh:nn');
            total.date = mcService.formatDate(new Date(), 'dd.mm.yyyy');
        } else {
            total.date = (total.time.length >= 6) ? total.time[0] + "." + total.time[1] + "." + total.time[2] : total.time;
            total.time = (total.time.length >= 6) ? total.time[3] + ":" + total.time[4] : total.time;
        }

        total.div.innerHTML = mcService.myReplaceFormated(
            '<div class="relative _messagesFontSize">' +
            "<span class='messageTime' title='#{timeTitle}'> #{time}&nbsp;</span>" +
            '<span class="messageText messageTextP60"><i class="fa fa-bullhorn" aria-hidden="true"></i> #{text}</span> ' +
            '</div>',
            {
                time     : total.time,
                timeTitle: total.date + " " + total.time,
                text     : msg.MsgType === mcConst._CMD_.msgType.MEDIA_CONF_START ?  mcLang(758) : mcLang(759, [mcService.MyFormatTime(_data.Duration)]) // "758":"Началась голосовая конференция", "759":"Закончилась голосовая конференция, длительность: ",
            }
        );

        if (msg.Idx) {
            total.div.setAttribute('Idx', msg.Idx);
            total.div.setAttribute('id' , "conf-" + msg.UID + "-" + msg.Idx);
        }

        RENDER_MSG.message(msg, total, RENDER_MSG.cost.custom);

        if (!msg.noNotify && !msg.History) {
            __showNotify(total.notI, total.text, "", total.isConf, total.isConf ? msg.UID : total.uin, msg.noBlink);
        }
    }

    // =============================================

    function __kickUserFromChannel(msg/*, _user*/) {
        // ChangeID: 5
        // ClientType: 0
        // Idx: 2953
        // Msg: "3"
        // MsgType: 19
        // State: 0
        // UID: 1
        // UIN: 13041
        // dtUTC: "08.05.2020.17.43.23"

        var User = mcService.StringToObj(msg.Msg);

        $rootScope.$broadcast(window._messages_.dialogsList.confKickUser, [{
            History          : msg.History,
            UID              : msg.UID,             // идентификатор текстового канала, из которого выгоняется пользователь
            UINKicker        : msg.UIN,             // UIN того, кто выгоняет другого пользователя
            // ModerDisplayName : _user.DisplayName,// (5.24+) отображаемое имя модератора
            ModerDisplayName : msg.Nick,            // (5.24+) отображаемое имя модератора
            UIN              : User.UIN,            // идентификатор изгоняемого пользователя
            UserDisplayName  : User.DisplayName,    // (5.24+) отображаемое имя пользователя, которого изгоняют из конференции
            dtUTC            : msg.dtUTC || msg.dt, // (6.1+) дата и время сообщения (UTC)
            Idx              : msg.Idx,             // (6.1+) внутренний индекс сообщения
            ClientType       : msg.ClientType       // (6.1+) тип клиента
        }]);
    }

    function __removeMsgList(frame, idUinOrUid, list, type) {
        if (frame && frame.firstChild){
            var msgList = frame.firstChild;
            var id      = null;
            var el      = null;

            while (list.length){
                id = list.pop();
                el = document.getElementById(type + idUinOrUid + "-" + id);

                if (el && el.nextSibling){
                    if (el.nextSibling.className === "spanBr"){
                        msgList.removeChild(el.nextSibling);
                    }

                    msgList.removeChild(el);
                }
            }
        }
    }

    // ======= DRAW MESSAGES =======================

    this.showNotify    = __showNotify;

    this.chatMessage   = function (msg, currentFrame, frameNode) {
        var frame;
        var _data;

        if (!msg.hasOwnProperty('UID')){
            frame = $rootScope.createUIN(__getUinWith(msg));
        } else {
            frame = $rootScope.createUID(msg.UID);
        }

        if (Self.framesList && !Self.framesList.hasFrame(frame)) {
            Self.framesList.create(frame).hide();
        }

        msg.Idx = msg.Idx || msg.MsgIdx;

        // if (!msg.History && msg.UINFrom == mcConst.UserInfo.UIN) {
        //     if ($rootScope.$broadcast(window._messages_.dialogsList.getUserState, [__getUinWith(msg)]) === mcConst.states.offline) {
        //         __addCustomMSG(mcLang(572), frame, true); // "572":"Пользователь сейчас не в сети, ваше сообщение будет доставлено",
        //     }
        // }

        var scrollHeight = 0;
        var __scrollDown = true;

        switch (msg.MsgType){
            case mcConst._CMD_.msgType.OLD_SHIT:
            case mcConst._CMD_.msgType.TEXT:
            case mcConst._CMD_.msgType.REPLY:
            case mcConst._CMD_.msgType.EXCEL_NEW:
            case mcConst._CMD_.msgType.IMAGE_NEW:
            case mcConst._CMD_.msgType.IMAGE:
            case mcConst._CMD_.msgType.INTEGRATION_API:
            case mcConst._CMD_.msgType.FTP_PUBLIC:
            case mcConst._CMD_.msgType.FTP_PERSONAL:
            case mcConst._CMD_.msgType.FILE_NEW:
            case mcConst._CMD_.msgType.FILE:
            case mcConst._CMD_.msgType.ANY_LINK:
            case mcConst._CMD_.msgType.LAN_LINK:
            case mcConst._CMD_.msgType.BARCODE:
            case mcConst._CMD_.msgType.BROADCAST_NOTIFY:
            case mcConst._CMD_.msgType.CLIPBOARD_EXCEL:
            case mcConst._CMD_.msgType.MYCHAT_GUEST:
                if (msg.Mod !== mcConst._CMD_.msgMods.DELETED){
                    if (msg.UINFrom == mcConst.UserInfo.UIN) {
                        __myMessages = $rootScope.$broadcast(window._messages_.dialogsList.getMyMessagesState, [msg.UINTo]);
                    }

                    var frameID = $rootScope.GetChatTypeID();

                    if (msg.History && delMyMsgList[frameID] && delMyMsgList[frameID][msg.Idx]){
                        delete delMyMsgList[frameID][msg.Idx];
                    } else {
                        scrollHeight = __addMSG(msg, frame);
                    }

                    // Self.noScrollDownOnSync = msg.History && !frameNode.scrollTop;
                    // Self.noScrollDownOnSync = msg.noScrollDown;
                }

                if (!msg.History &&
                    (
                        (mcConst.ClientSettings.EventsPopupOnPrivateMessage == 1 && !msg.hasOwnProperty('UID')) ||
                        (mcConst.ClientSettings.EventsPopupOnChannelMessage == 1 &&  msg.hasOwnProperty('UID')))
                    )
                {
                    $rootScope.$broadcast(window._messages_.dialogsCtrl.selectDialog, [frame]);

                    if (!$rootScope.isWebClient) {
                        $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                            mcConst._CMD_.ce_show_on_top
                        ]);
                    }
                }
            break;

            case mcConst._CMD_.msgType.MEDIA_CALL:
                // console.log(msg);

                $rootScope.$broadcast(window._messages_.callManager.mediaCallMessage, [msg]);

                scrollHeight = __addMSG(msg, frame);
            break;

            case mcConst._CMD_.msgType.CONF_JUST_CREATED:
                if (msg.UINFrom != mcConst.UserInfo.UIN && msg.showNotify){
                    _data = mcService.StringToObj(msg.Msg);

                    __showNotify(true, mcLang(702, [_data.Name]), $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [msg.UINFrom]).DisplayName, true, msg.UID); // "702":"Вас пригласили в конференцию %s",
                }
            break;

            case mcConst._CMD_.msgType.SOURCE_CODE:
                __codeSnippet(msg, frame);
            break;

            case mcConst._CMD_.msgType.KANBAN_NOTIFY:
                __addKanbanMSG(msg, frame);
            break;

            case mcConst._CMD_.msgType.FORUM_NOTIFY:
                __addForumMSG(msg, frame);
            break;

            case mcConst._CMD_.msgType.BEEP:
                // addKanbanMSG(msg, frame);
                __scrollDown = false;

                if (!msg.History && msg.UINFrom != mcConst.UserInfo.UIN) {
                    mcPlaySound.PlaySound(mcPlaySound.Sounds.BEEP, true);

                    !$rootScope.isWebClient && $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                        mcConst._CMD_.ce_show_on_top
                    ]);
                    
                    $rootScope.$broadcast('OpenPrivate', [msg.UINFrom]);
                }
            break;

            case mcConst._CMD_.msgType.CONF_BAN:
                __addBanUser(msg, frame);
            break;

            case mcConst._CMD_.msgType.CONF_TURN_OUT:
                if (!msg.History){
                    __kickUserFromChannel(msg);
                }
            break;

            case mcConst._CMD_.msgType.CONF_CHANGE_TOPIC:
                __scrollDown = false;
            break;

            case mcConst._CMD_.msgType.CONF_INVITED:
                _data = mcService.StringToObj(msg.Msg);

                var uins  = _data.UINS.split(',');
                var names = [];

                uins.forEach(function () {
                    // var name = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [uin]).DisplayName;
                    // names.push(name === "noNameUser" ? mcLang(723) : name);
                    names.push("noNameUser");
                });

                msg.nick = msg.Nick || $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [msg.UIN]).DisplayName;
                msg.Text = _data.UINS == mcConst.UserInfo.UIN
                    ? mcLang(715)                   // "715":"Вас пригласили в конференцию",
                    : mcLang(716, names.join(',')); // "716":"%s пригласили в конференцию", "723":"Пользователя",

                __addCustomMSG(msg, frame); //

                if (_data.UINS != mcConst.UserInfo.UIN) {
                    setTimeout(function (){ // обновляем список имен приглашенных пользователей в конфу, обычно там юзера, которых у нас нет
                        $rootScope.$broadcast(window._messages_.usersSmallInfo.updateUsersSmallInfo, [uins, function (users){
                            names = [];

                            users.forEach(function (user){
                                names.push(user.DisplayName);
                            });

                            var res = mcLang(716, names.join(',')); // "716":"%s пригласили в конференцию", "723":"Пользователя",
                            var mm  = document.getElementById("conf-" + msg.UID + "-" + msg.Idx);

                            if (mm){
                                var mmm = mm.querySelector(".messageText");

                                try {
                                    mm.firstChild.removeChild(mmm);
                                    mm.innerHTML += '<span class="messageText messageTextP60"><i class="fa fa-bullhorn" aria-hidden="true"></i> ' + res + '</span>';
                                } catch (e){}
                            }
                        }]);
                    }, 200);
                }
            break;

            case mcConst._CMD_.msgType.CONF_ALERT:
            case mcConst._CMD_.msgType.CONF_PERSONAL:
                // мусорные команды - выкинуть нах с сервера
            break;

            case mcConst._CMD_.msgType.ADM_DELETE_MESSAGE:
                __removeMSG(msg, frame);

                __scrollDown = false;
            break;

            case mcConst._CMD_.msgType.TEXT_MODIFY:
                if (!msg.History){
                    __editMSG(msg, frame);
                }

                __scrollDown = false;
            break;

            case mcConst._CMD_.msgType.PRIVATE_REDIRECT:
                var redirectData = mcService.StringToObj(msg.Msg);

                if (!msg.History && redirectData.UIN == mcConst.UserInfo.UIN &&  redirectData.UINTo != $rootScope.GetChatID()) {
                    $rootScope.$broadcast('OpenPrivate', [redirectData.UINTo]);
                }

                __addRedirectMSG(msg, frame);
            break;

            case mcConst._CMD_.msgType.MEDIA_CONF_START:
            case mcConst._CMD_.msgType.MEDIA_CONF_CLOSE:
                _data = mcService.StringToObj(msg.Msg);

                switch (_data.Type) {
                    case mcConst.mediaConf.VIDEO:
                    break;

                    case mcConst.mediaConf.VOICE:
                        __mediaConfMSG(msg, frame, _data);
                    break;

                    case mcConst.mediaConf.WEBINAR:
                    break;
                }
            break;

            default:
                __addCustomMSG(msg, frame);
        }

        // if (!msg.History && __scrollDown && (currentFrame == frame || currentFrame.id == frame)){
        //     Self.scrollDown(false, !msg.History, msg.History && msg.noScrollDown, scrollHeight);
        // }
    };

    this.customMessage = function customMessage(msg, currentFrame, frameNode) {
        var frame = null;

        if (msg.MsgType == mcConst._CMD_.msgType.KANBAN_NOTIFY){
            frame = $rootScope.chatAliases.Elisa;
        } else {
            frame = msg.hasOwnProperty('UIN') ? "UIN-" + msg.UIN : "UID-" + msg.UID;
        }

        if (Self.framesList && !Self.framesList.hasFrame(frame)) {
            Self.framesList.create(frame).hide();
        }

        if (msg.isTechMsg) {
            if (msg.Place == 1) {
                msg.UIN = msg.ID;

                frame =  "UIN-" + msg.UIN;

                $rootScope.$broadcast('OpenPrivate', [msg.UIN, function (){
                    __addTechMSG(msg, frame);
                }]);
            } else {
                msg.UID = msg.ID;

                frame =  "UID-" + msg.UID;

                $rootScope.$broadcast('OpenConf', [msg.UID]);
                
                __addTechMSG(msg, frame);
            }

            if (msg.StayOnTop){
                $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                    mcConst._CMD_.ce_show_on_top
                ]);
            }
        } else
        if (msg.MsgType == mcConst._CMD_.msgType.KANBAN_NOTIFY) {
            $rootScope.$broadcast('updateCounter', [msg]);

            __addKanbanMSG(msg, frame);
        } else
        if (msg.MsgType === mcConst._CMD_.msgType.SHOW_RECEIVED_FILES){
            __addReceivedFilesMSG(msg, frame);
        } else
        /*if (msg.MsgType === mcConst._CMD_.msgType.MEDIA_CONF_START) {
            __addCustomMSG(msg, frame);
            var info = mcService.StringToObj(msg.Msg);

            switch (info.Type) {
                case mcConst.mediaConf.VOICE:
                    mcAudioConf.joinToConf(info);
                break;

                case mcConst.mediaConf.VIDEO:

                break;

                case mcConst.mediaConf.WEBINAR:

                break;
            }
        } else*/
        if (msg.MsgType === mcConst._CMD_.msgType.SHOW_UPLOADED_FILES){
            __addReceivedFilesMSG(msg, frame);
        } else
        /*if (msg.MsgType === mcConst._CMD_.msgType.CONF_INVITED) {
            var _data = mcService.StringToObj(msg.Msg);
            var uins  = _data.UIS.split(',');

            __addForumMSG(msg, frame); // "715":"Вас пригласили в конференцию",
        } else*/ {
            __addCustomMSG(msg, frame);
        }

        // Self.noScrollDownOnSync = msg.History && !frameNode.scrollTop;
        // Self.noScrollDownOnSync = msg.scrollDown;

        // if (currentFrame == frame || currentFrame.id == frame){
        // if (msg.scrollDown && (currentFrame == frame || currentFrame.id == frame)){
        //     Self.scrollDown(false, !msg.History, msg.History && msg.noScrollDown);
        // }
    };

    this.getMyMessagesTemp = function () {
        return {
            UIN        : -1,
            IDMyMsgGot : 0,
            IDMyMsgRead: 0
        }
    };

    this.setMyMessages = function (data) {
        __myMessages = data;
    };

    this.removeMsgList = __removeMsgList;

    this.framesList     = null;
    this.loadAllImages  = loadImages.loadAllImages;
    this.scrollDown     = null;
}



function mcEmojiList() {
    var packageList = {
        "1" : [
            "&#x1F600|grinning face|:-) :) =) :c) :o) :] 8) :?) :^) :} :grinning:|smiling,face,smile,happy,joy",
            "&#x1F603|grinning face with big eyes|:smiley: :-D :D",
            "&#x1F604|grinning face with smiling eyes|:smile:",
            "&#x1F601|beaming face with smiling eyes|:grin:",
            "&#x1F606|grinning squinting face|)) XD xD :satisfied:",
            "&#x1F605|grinning face with sweat|:sweat_smile:",
            "&#x1F602|face with tears of joy|:'-) :'-D :joy:",
            // "&#x263A|smiling face|:relaxed:",
            "&#x1F60A|smiling face with smiling eyes|:blush:",
            "&#x1F607|smiling face with halo|:innocent:",
            "&#x1F642|slightly smiling face|:slightly_smiling_face:",
            "&#x1F643|upside-down face|:upside_down_face:",
            "&#x1F609|winking face|:wink:",
            "&#x1F60C|relieved face|:relieved:",
            "&#x1F60D|smiling face with heart-eyes|:heart_eyes:",
            "&#x1F618|face blowing a kiss|:kissing_heart:",
            "&#x1F617|kissing face|:kissing:",
            "&#x1F619|kissing face with smiling eyes|:kissing_smiling_eyes:",
            "&#x1F61A|kissing face with closed eyes|:kissing_closed_eyes:",
            "&#x1F60B|face savoring food|:-Ъ :yum:",
            "&#x1F61B|face with tongue|:-P :stuck_out_tongue:",
            "&#x1F61D|squinting face with tongue|:stuck_out_tongue_closed_eyes:",
            "&#x1F61C|winking face with tongue|:stuck_out_tongue_winking_eye:",
            "&#x1F913|nerd face|:nerd_face:",
            "&#x1F60E|smiling face with sunglasses|B-) :sunglasses:",
            "&#x1F60F|smirking face|:smirk:",
            "&#x1F612|unamused face|:unamused:",
            "&#x1F61E|disappointed face|:disappointed:",
            "&#x1F614|pensive face|:pensive:",
            "&#x1F61F|worried face|:worried:",
            "&#x1F615|confused face|:confused:",
            "&#x1F641|slightly frowning face|:slightly_frowning_face:",
            "&#x2639|frowning face|:white_frowing_face:",
            "&#x1F623|persevering face|:persevere:",
            "&#x1F616|confounded face|:confounded:",
            "&#x1F62B|tired face|:tired_face:",
            "&#x1F629|weary face|:weary:",
            "&#x1F622|crying face|:cry:",
            "&#x1F62D|loudly crying face|:sob:",
            "&#x1F624|face with steam from nose|:triumph:",
            "&#x1F620|angry face|:angry:",
            "&#x1F621|pouting face|:rage:",
            "&#x1F633|flushed face|:flushed:",
            "&#x1F631|face screaming in fear|:scream:",
            "&#x1F628|fearful face|:fearful:",
            "&#x1F630|anxious face with sweat|:cold_sweat:",
            "&#x1F625|sad but relieved face|:disappointed_relieved:",
            "&#x1F613|downcast face with sweat|:sweat:",
            "&#x1F917|hugging face|:hugging_face:",
            "&#x1F914|thinking face|:hmm:",
            "&#x1F636|face without mouth|:no_mouth:",
            "&#x1F610|neutral face|:neutral_face:",
            "&#x1F611|expressionless face|:expressionless:",
            "&#x1F62C|grimacing face|:grimacing:",
            "&#x1F644|face with rolling eyes|:face_with_rolling_eyes:",
            "&#x1F62F|hushed face|:hushed:",
            "&#x1F626|frowning face with open mouth|:frowning:",
            "&#x1F627|anguished face|:anguished:",
            "&#x1F62E|face with open mouth|:O :open_mouth:",
            "&#x1F632|astonished face|:astonished:",
            "&#x1F634|sleeping face|:sleeping:",
            "&#x1F62A|sleepy face|:sleepy:",
            "&#x1F635|dizzy face|:dizzy_face:",
            "&#x1F910|zipper-mouth face|:zipper_mouth_face:",
            "&#x1F637|face with medical mask|:mask:",
            "&#x1F912|face with thermometer|:face_with_thermometer:",
            "&#x1F915|face with head-bandage|:face_with_head_bandage:",
            "&#x1F911|money-mouth face|:money_mouth_face",
            "&#x1F608|smiling face with horns|>:-D >:) :smiling_imp:",
            "&#x1F47F|angry face with horns|:imp:",
            "&#x1F479|ogre|:japanese_ogre:",
            "&#x1F47A|goblin|:japanese_goblin:",
            "&#x1F4A9|pile of poo|:shit: :pile_of_poo: :poop: :hankey:",
            "&#x1F47B|ghost|:ghost:",
            "&#x1F480|skull|:skull:",
            "&#x2620|skull and crossbones|:skull_and_crossbones:",
            "&#x1F47D|alien|:alien:",
            "&#x1F47E|alien monster|:space_invader:",
            "&#x1F916|robot|:robot:",
            "&#x1F63A|grinning cat|:smiley_cat:",
            "&#x1F638|grinning cat with smiling eyes|:smile_cat:",
            "&#x1F639|cat with tears of joy|:joy_cat:",
            "&#x1F63B|smiling cat with heart-eyes|:heart_eyes_cat:",
            "&#x1F63C|cat with wry smile|:smirk_cat:",
            "&#x1F63D|kissing cat|:kissing_cat:",
            "&#x1F640|weary cat|:scream_cat:",
            "&#x1F63F|crying cat|:crying_cat_face:",
            "&#x1F63E|pouting cat|:pouting_cat:"
        ],

        "2" : [
            "&#x1F5E3|speaking head|:speaking_head_in_silhouette:",
            "&#x1F464|bust in silhouette|:bust_in_silhouette:",
            "&#x1F465|busts in silhouette|:busts_in_silhouette:",
            "&#x1F476|baby|:baby:",
            "&#x1F467|girl|:girl:",
            "&#x1F466|boy|:boy:",
            "&#x1F469|woman|:woman:",
            "&#x1F468|man|:man:",
            "&#x1F471|person with blond hair|:person_with_blond_hair:",
            "&#x1F475|old woman|:older_woman: :grandma:",
            "&#x1F474|old man|:older_man:",
            "&#x1F472|person with skullcap|:man_with_gua_pi_mao:",
            "&#x1F473|person wearing turban|:man_with_turban:",
            "&#x1F46E|police officer|:cop:",
            "&#x1F477|construction worker|:construction_worker:",
            "&#x1F482|guard|:guardsman: :man_guard:",
            "&#x1F575|detective|:sleuth_or_spy:",
            "&#x1F470|person with veil|:bride_with_veil:",
            "&#x1F478|princess|:princess:",
            "&#x1F385|Santa Claus|:santa:",
            "&#x1F47C|baby angel|:baby_angel:",
            "&#x1F647|person bowing|:bow:",
            "&#x1F481|person tipping hand|:information_desk_person:",
            "&#x1F645|person gesturing no|:no_good:",
            "&#x1F646|person gesturing OK|:okay_woman:",
            "&#x1F64B|person raising hand|:raising_hand:",
            "&#x1F64E|person pouting|:person_with_pouting_face:",
            "&#x1F64D|person frowning|:person_frowning:",
            "&#x1F487|person getting haircut|:haircut:",
            "&#x1F486|person getting massage|:massage:",
            "&#x1F46F|people with bunny ears|:dancers:",
            "&#x1F6B6|person walking|:walking:",
            "&#x1F3C3|person running|:running:",
            "&#x1F46B|woman and man holding hands|:couple:",
            "&#x1F46D|women holding hands|:two_women_holding_hands:",
            "&#x1F46C|men holding hands|:two_men_holding_hands:",
            "&#x1F491|couple with heart|:couple_with_heart:",
            "&#x1F48F|kiss|:couplekiss:",
            "&#x1F46A|family|:family:",
            "&#x1F3C2|snowboarder|:snowboarder:",
            "&#x1F3C4|woman surfing|:woman_surfing:",
            "&#x1F3C7|horse racing|:horse_racing:",
            "&#x1F3CA|person swimming|:swimmer:",
            "&#x1F3CB|person lifting weights|:weight_lifter:",
            "&#x1F3CC|person golfing|:golfer:",
            "&#x1F6A3|person rowing boat|:rowboat:",
            "&#x1F6B4|person biking|:bicyclist:",
            "&#x1F6B5|person mountain biking|:mountain_bicyclist:",
            "&#x26F7|skier|:skier:",
            "&#x26F9|person bouncing ball|:person_with_ball:",
        ],

        "3" : [
            "&#x1F450|open hands|:open_hands:",
            "&#x1F64C|raising hands|:raised_hands:",
            "&#x1F44F|clapping hands|:clap:",
            "&#x1F44D|thumbs up|:thumbsup: :+1: :like:",
            "&#x1F44E|thumbs down|:thumbsdown: :-1: :dislike:",
            "&#x1F44A|oncoming fist|:punch: :facepunch:",
            "&#x270A|raised fist|:fist:",
            "&#x270C|victory hand|:v:",
            "&#x1F918|sign of the horns|:sign_of_the_horns:",
            "&#x1F44C|OK hand|:ok_hand:",
            "&#x1F448|backhand index pointing left|:point_left:",
            "&#x1F449|backhand index pointing right|:point_right:",
            "&#x1F446|backhand index pointing up|:point_up_2:",
            "&#x1F447|backhand index pointing down|:point_down:",
            "&#x261D|index pointing up|:point_up:",
            "&#x270B|raised hand|:hand: :raised_hand:",
            "&#x1F590|hand with fingers splayed|:raised_hand_with_fingers_splayed:",
            "&#x1F596|vulcan salute|:raised_hand_with_part_between_middle_and_ring_fingers:",
            "&#x1F44B|waving hand|:wave:",
            "&#x1F4AA|flexed biceps|:muscle:",
            "&#x1F595|middle finger|:fu: :reversed_hand_with_middle_finger_extended:",
            "&#x270D|writing hand|:writing_hand:",
            "&#x1F64F|folded hands|:pray:",
            "&#x1F485|nail polish|:nail_care:",
        ],

        "4" : [
            "&#x1F34F|green apple|:green_apple:",
            "&#x1F34E|red apple|:apple:",
            "&#x1F350|pear|:pear:",
            "&#x1F34A|tangerine|:tangerine:",
            "&#x1F34B|lemon|:lemon:",
            "&#x1F34C|banana|:banana:",
            "&#x1F349|watermelon|:watermelon:",
            "&#x1F347|grapes|:grapes:",
            "&#x1F353|strawberry|:strawberry:",
            "&#x1F348|melon|:melon:",
            "&#x1F352|cherries|:cherries:",
            "&#x1F351|peach|:peach:",
            "&#x1F34D|pineapple|:pineapple:",
            "&#x1F345|tomato|:tomato:",
            "&#x1F346|eggplant|:eggplant:",
            "&#x1F336|hot pepper|:hot_pepper:",
            "&#x1F33D|ear of corn|:corn:",
            "&#x1F360|roasted sweet potato|:sweet_potato:",
            "&#x1F35E|bread|:bread:",
            "&#x1F9C0|cheese wedge|:cheese_wedge:",
            "&#x1F373|cooking|:cooking:",
            "&#x1F357|poultry leg|:poultry_leg:",
            "&#x1F356|meat on bone|:meat_on_bone:",
            "&#x1F32D|hot dog|:hot_dog:",
            "&#x1F354|hamburger|:hamburger:",
            "&#x1F35F|french fries|:fries:",
            "&#x1F355|pizza|:pizza:",
            "&#x1F32E|taco|:taco:",
            "&#x1F32F|burrito|:burrito:",
            "&#x1F35D|spaghetti|:spaghetti:",
            "&#x1F35C|steaming bowl|:ramen:",
            "&#x1F372|pot of food|:stew:",
            "&#x1F35B|curry rice|:curry:",
            "&#x1F363|sushi|:sushi:",
            "&#x1F371|bento box|:bento:",
            "&#x1F364|fried shrimp|:fried_shrimp:",
            "&#x1F359|rice ball|:rice_ball:",
            "&#x1F35A|cooked rice|:rice:",
            "&#x1F358|rice cracker|:rice_cracker:",
            "&#x1F365|fish cake with swirl|:fish_cake:",
            "&#x1F362|oden|:oden:",
            "&#x1F361|dango|:dango:",
            "&#x1F367|shaved ice|:shaved_ice:",
            "&#x1F368|ice cream|:ice_cream:",
            "&#x1F366|soft ice cream|:icecream:",
            "&#x1F370|shortcake|:cake:",
            "&#x1F36E|custard|:flan:",
            "&#x1F36D|lollipop|:lollipop:",
            "&#x1F36C|candy|:candy:",
            "&#x1F36B|chocolate bar|:chocolate_bar:",
            "&#x1F37F|popcorn|:popcorn:",
            "&#x1F369|doughnut|:doughnut:",
            "&#x1F36A|cookie|:cookie:",
            "&#x1F330|chestnut|:chestnut:",
            "&#x1F36F|honey pot|:honey_pot:",
            "&#x1F37C|baby bottle|:baby_bottle:",
            "&#x1F375|teacup without handle|:tea:",
            "&#x1F376|sake|:sake:",
            "&#x1F37A|beer mug|:beer:",
            "&#x1F37B|clinking beer mugs|:beers:",
            "&#x1F377|wine glass|:wine_glass:",
            "&#x1F378|cocktail glass|:cocktail:",
            "&#x1F379|tropical drink|:tropical_drink:",
            "&#x1F37E|bottle with popping cork|:bottle_with_popping_cork:",
            "&#x1F374|fork and knife|:fork_and_knife:",
            "&#x1F37D|fork and knife with plate|:fork_and_knife_with_plate:",
        ],

        "5" : [
            "&#x1F436|dog face|:dog:",
            "&#x1F431|cat face|:cat:",
            "&#x1F42D|mouse face|:mouse:",
            "&#x1F439|hamster|:hamster:",
            "&#x1F430|rabbit face|:rabbit:",
            "&#x1F43B|bear|:bear:",
            "&#x1F43C|panda|:panda_face:",
            "&#x1F428|koala|:koala:",
            "&#x1F42F|tiger face|:tiger:",
            "&#x1F981|lion|:lion:",
            "&#x1F42E|cow face|:cow:",
            "&#x1F437|pig face|:pig:",
            "&#x1F43D|pig nose|:pig_nose:",
            "&#x1F438|frog|:frog:",
            "&#x1F435|monkey face|:monkey_face:",
            "&#x1F648|see-no-evil monkey|:see_no_evil:",
            "&#x1F649|hear-no-evil monkey|:hear_no_evil:",
            "&#x1F64A|speak-no-evil monkey|:speak_no_evil:",
            "&#x1F412|monkey|:monkey:",
            "&#x1F414|chicken|:chicken:",
            "&#x1F427|penguin|:penguin:",
            "&#x1F426|bird|:bird:",
            "&#x1F424|baby chick|:baby_chick:",
            "&#x1F423|hatching chick|:hatching_chick:",
            "&#x1F425|front-facing baby chick|:hatched_chick:",
            "&#x1F43A|wolf|:wolf:",
            "&#x1F417|boar|:boar:",
            "&#x1F434|horse face|:horse:",
            "&#x1F984|unicorn|:unicorn_face:",
            "&#x1F41D|honeybee|:honeybee: :bee:",
            "&#x1F41B|bug|:bug:",
            "&#x1F40C|snail|:snail:",
            "&#x1F41E|lady beetle|:beetle:",
            "&#x1F41C|ant|:ant:",
            "&#x1F982|scorpion|:scorpion:",
            "&#x1F422|turtle|:turtle:",
            "&#x1F40D|snake|:snake:",
            "&#x1F419|octopus|:octopus:",
            "&#x1F980|crab|:crab:",
            "&#x1F421|blowfish|:blowfish:",
            "&#x1F420|tropical fish|:tropical_fish:",
            "&#x1F41F|fish|:fish:",
            "&#x1F42C|dolphin|:dolphin:",
            "&#x1F433|spouting whale|:whale:",
            "&#x1F40B|whale|:whale2:",
            "&#x1F40A|crocodile|:crocodile:",
            "&#x1F405|tiger|:tiger2:",
            "&#x1F406|leopard|:leopard:",
            "&#x1F418|elephant|:elepchant:",
            "&#x1F42A|camel|:dromedary_camel:",
            "&#x1F42B|two-hump camel|:camel",
            "&#x1F403|water buffalo|:water_buffalo:",
            "&#x1F402|ox|:ox:",
            "&#x1F404|cow|:cow2:",
            "&#x1F40E|horse|:racehorse:",
            "&#x1F416|pig|:pig2:",
            "&#x1F40F|ram|:ram:",
            "&#x1F411|sheep|:sheep:",
            "&#x1F410|goat|:goat:",
            "&#x1F415|dog|:dog2:",
            "&#x1F429|poodle|:poodle:",
            "&#x1F408|cat|:cat2:",
            "&#x1F413|rooster|:rooster:",
            "&#x1F983|turkey|:turkey:",
            "&#x1F407|rabbit|:rabbit2:",
            "&#x1F401|mouse|:mouse2:",
            "&#x1F400|rat|:rat:",
            "&#x1F43F|chipmunk|:chipmunk:",
            "&#x1F409|dragon|:dragon:",
            "&#x1F432|dragon face|:dragon_face:",
            "&#x1F335|cactus|:cactus:",
            "&#x1F332|evergreen tree|:evergreen_tree:",
            "&#x1F333|deciduous tree|:deciduous_tree:",
            "&#x1F334|palm_tree|:palm_tree:",
            "&#x1F331|seedling|:seedling:",
            "&#x1F33F|herb|:herb:",
            "&#x2618|shamrock|:shamrock:",
            "&#x1F340|four leaf clover|:four_leaf_clover:",
            "&#x1F343|leaf fluttering in wind|:leaves:",
            "&#x1F342|fallen leaf|:fallen_leaf:",
            "&#x1F341|maple leaf|:maple_leaf:",
            "&#x1F344|mushroom|:mushroom:",
            "&#x1F41A|spiral shell|:shell:",
            "&#x1F33E|sheaf of rice|:ear_of_rice:",
            "&#x1F337|tulip|:tulip:",
            "&#x1F339|rose|:rose:",
            "&#x1F33A|hibiscus|:hibiscus:",
            "&#x1F338|cherry blossom|:cherry_blossom:",
            "&#x1F33C|blossom|:blossom:",
            "&#x1F33B|sunflower|:sunflower:",
            "&#x1F31E|sun with face|:sun_with_face:",
            "&#x1F31D|full moon face|:full_moon_with_face:",
            "&#x1F31B|first quarter moon face|:first_quarter_moon_with_face:",
            "&#x1F31C|last quarter moon face|:last_quarter_moon_with_face:",
            "&#x1F31A|new moon face|:new_moon_with_face:",
            "&#x1F315|full moon|:full moon:",
            "&#x1F316|waning gibbous moon|:waning_gibbous_moon:",
            "&#x1F317|last quarter moon|:last_quarter_moon:",
            "&#x1F318|waning crescent moon|:waning_crescent_moon:",
            "&#x1F311|new moon|:new_moon:",
            "&#x1F312|waxing crescent moon|:waxing_crescent_moon:",
            "&#x1F313|first quarter moon|:first_quarter_moon:",
            "&#x1F314|waxing gibbous moon|:waxing_gibbous_moon",
            "&#x1F319|crescent moon|:crescent_moon:",
            "&#x1F30E|globe showing Americas|:earth_americas:",
            "&#x1F30D|globe showing Europe-Africa|:earth_africa:",
            "&#x1F30F|globe showing Asia-Australia|:earth_asia:",
            "&#x2604|comet|:comet:",
            "&#x1F32A|tornado|:cloud_with_tornado:",
            "&#x2600|sun|:sunny:",
            "&#x1F324|sun behind small cloud|:white_sun_behind_small_cloud:",
            "&#x26C5|sun behind cloud|:partly_sunny:",
            "&#x1F325|sun behind large cloud|:white_sun_behind_cloud:",
            "&#x2601|cloud|:cloud:",
            "&#x1F326|sun behind rain cloud|:white_sun_behind_cloud_with_rain:",
            "&#x1F327|cloud with rain|:cloud_with_rain:",
            "&#x26C8|cloud with lightning and rain|:thunder_cloud_with_rain:",
            "&#x1F329|cloud with lightning|:cloud_with_lightning:",
            "&#x1F328|cloud with snow|:cloud_with_snow:",
            "&#x2744|snowflake|:snowflake:",
        ],

        "6" : [
            "&#x1F6B2|bycicle|:bike:",
            "&#x1F3CD|motorcycle|:racing_motocicle:",
            "&#x1F3CE|racing car|:racing_car:",
            "&#x1F699|sport utility vehicle|:blue_car:",
            "&#x1F697|automobile|:car: :red_car:",
            "&#x1F695|taxi|:taxi:",
            "&#x1F698|oncoming automobile|:oncoming_automobile:",
            "&#x1F696|oncoming taxi|:oncoming_taxi:",
            "&#x1F68D|oncoming bus|:oncoming_bus:",
            "&#x1F68C|bus|:bus:",
            "&#x1F6A8|police car light|:rotating_light:",
            "&#x1F694|oncoming police car|:oncoming_police_car:",
            "&#x1F693|police car|:police_car:",
            "&#x1F692|fire engine|:fire_engine:",
            "&#x1F691|ambulance|:ambulance:",
            "&#x1F690|minibus|:minibus:",
            "&#x1F68E|trolleybus|:trolleybus:",
            "&#x1F69B|articulated lorry|:articulated_lorry:",
            "&#x1F69A|delivery truck|:truck:",
            "&#x1F683|railway car|:railway_car:",
            "&#x1F68B|tram car|:tram_car:",
            "&#x1F69F|suspension railway|:suspension_railway:",
            "&#x1F69E|mountain railway|:mountain_railway:",
            "&#x1F6A1|aerial tramway|:aerial_tramway:",
            "&#x1F6A0|mountain cableway|:mountain_cableway:",
            "&#x1F689|station|:station:",
            "&#x1F68A|tram|:tram:",
            "&#x1F687|metro|:metro:",
            "&#x1F688|light rail|:light_rail:",
            "&#x1F686|train|:train2:",
            "&#x1F685|bullet train|:bullettrain_front:",
            "&#x1F684|high-speed train|:bullettrain_side:",
            "&#x1F69D|monorail|:monorail:",
            "&#x1F682|locomotive|:steam_locomotive:",
            "&#x1F6E4|railway track|:railroad_track:",
            "&#x1F6E3|motorway|:motorway:",
            "&#x1F69C|tractor|:tractor:",
            "&#x2708|airplane|:airplane:",
            "&#x1F6EC|airplane arrival|:airplane_arriving:",
            "&#x1F6EB|airplane departure|:airplane_departure:",
            "&#x1F681|helicopter|:helicopter:",
            "&#x1F680|rocket|:rocket:",
            "&#x1F6E9|small airplane|:small_airplane:",
            "&#x1F6F0|satellite|:satellite_orbital:",
            "&#x26F5|sailboat|:sailboat:",
            "&#x26F4|ferry|:ferry:",
            "&#x1F6F3|passenger ship|:passenger_ship:",
            "&#x1F6E5|motor boat|:motor_boat:",
            "&#x1F6A4|speedboat|:speedboat:",
            "&#x1F6A2|ship|:ship:",
            "&#x1F6A7|construction|:construction:",
            "&#x1F6A6|vertical traffic light|:vertical_traffic_light:",
            "&#x1F6A5|horizontal traffic light|:traffic_light:",
            "&#x1F310|globe with meridians|:globe_with_meridians:",
            "&#x1F5FB|mount Fuji|:mount_fuji:",
            "&#x1F5FC|Tokyo tower|:tokyo_tower:",
            "&#x1F5FD|Statue of Liberty|:statue_of_liberty:",
            "&#x1F5FF|moai|:moyai:",
            "&#x1F3A0|carousel horse|:carousel_horse:",
            "&#x1F54B|kaaba|:kaaba:",
            "&#x1F54C|mosque|:mosque:",
            "&#x1F54D|synagogue|:synagogue:",
            "&#x26E9|shinto shrine|:shinto_shrine:",
            "&#x26EA|church|:church:",
            "&#x26FA|tent|:tent:"
        ],

        "7" : [
            "&#x2764|red heart|:heart:",
            "&#x1F49B|yellow heart|:yellow_heart:",
            "&#x1F49A|green heart|:green_heart",
            "&#x1F499|blue heart|:blue_heart:",
            "&#x1F49C|purple_heart|:purple_heart:",
            "&#x1F494|broken heart|:broken_heart:",
            "&#x2763|heart exclamation|:heavy_heart_exclamation_mark_ornament:",
            "&#x1F495|two hearts|:two_hearts:",
            "&#x1F49E|revolving hearts|:revolving_hearts:",
            "&#x1F493|beating heart|:heartheat:",
            "&#x1F497|growing heart|:heartpulse:",
            "&#x1F496|sparkling heart|:sparkling_heart:",
            "&#x1F498|heart with arrow|:heart_with_arrow:",
            "&#x1F49D|heart with ribbon|:gift_heart:",
            "&#x1F49F|heart decoration|:heart_decoration:",
            "&#x262E|peace symbol|:peace_symbol:",
            "&#x271D|latin cross|:latin_cross:",
            "&#x262A|star and crescent|:star and crescent:",
            "&#x1F549|om|:om_symbol:",
            "&#x2638|wheel of dharma|:wheel_of_dharma:",
            "&#x2721|star of David|:star_of_david:",
            "&#x1F52F|dotted six-pointed star|:six_pointed_star:",
            "&#x1F54E|menorah|:menorah:",
            "&#x262F|yin yang|:yin_yang:",
            "&#x2626|orthodox cross|:orthodox_cross:",
            "&#x1F6D0|place of worship|:worship_symbol:",
            "&#x26CE|Ophiuchus|:ophiuchus:",
            "&#x2648|Aries|:aries:",
            "&#x2649|Taurus|:taurus:",
            "&#x264A|Gemini|:gemini:",
            "&#x264B|Cancer|:cancer:",
            "&#x264C|Leo|:leo:",
            "&#x264D|Virgo|:virgo:",
            "&#x264E|Libra|:libra:",
            "&#x264F|Scorpio|:scorpius:",
            "&#x2650|Sagittarius|:sagittarius:",
            "&#x2651|Capricorn|:capricorn:",
            "&#x2652|Aquarius|:aquarius:",
            "&#x2653|Pisces|:pisces:",
            "&#x1F194|ID button|:id:",
            "&#x269B|atom symbol|:atom_symbol:",
            "&#x1F251|Japanese acceptable button|:acceptable:",
            "&#x2622|radioactive|:radioactive_sign:",
            "&#x2623|biohazard|:biohazard_sign:",
            "&#x1F4F4|mobile phone off|:mobile_phone_off:",
            "&#x1F4F3|vibration mode|:vibration_mode:",
            "&#x1F236|Japanese not free of charge button|:u6709:",
            "&#x1F21A|Japanese free of charge  button|:u7121:",
            "&#x1F238|Japanese application button|:u7533:",
            "&#x1F23A|Japanese open for business button|:u55b6:",
            "&#x1F237|Japanese monthly amount button|:u6708:",
            "&#x2734|eight pointed star|:eight_pointed_black_star:",
            "&#x1F19A|vs button|:vs:",
            "&#x1F250|Japanese bargain button|:ideograph_advantage:",
            "&#x3299|Japanese secret button|:secret:",
            "&#x3297|Japanese congratulations button|:congratulations:",
            "&#x1F234|Japanese passing grade button|:u5408:",
            "&#x1F235|Japanese no vacancy button|:u6e80:",
            "&#x1F239|Japanese discount button|:u5272:",
            "&#x1F232|Japanese prohibited button|:u7981:",
            "&#x1F170|A button blood type|:a:",
            "&#x1F171|B button blood type|:b:",
            "&#x1F18E|AB button blood type|:ab:",
            "&#x1F191|CL button|:cl:",
            "&#x1F17E|O button blood type|:o2:",
            "&#x1F198|SOS button|:sos:",
            "&#x274C|cross mark|:x:",
            "&#x1F534|red circle|:stop_sign:",
            "&#x26D4|no entry|:no_entry:",
            "&#x1F4DB|name badge|:name_budge:",
            "&#x1F6AB|prohibited|:no_entry_sign:",
            "&#x1F4AF|hundred points|:100:",
            "&#x1F4A2|anger symbol|:anger:",
            "&#x2668|hot springs|:hot_springs:",
            "&#x1F6B7|no pedestrians|:no_pedestrians:",
            "&#x1F6AF|no littering|:no_littering:",
            "&#x1F6B3|no bicycles|:no_bicycles:",
            "&#x1F6B1|non-potable water|:non_potable_water:",
            "&#x1F51E|no one under eighteen|:underage:",
            "&#x1F4F5|no mobile phones|:no_mobile_phones:",
            "&#x1F6AD|no smoking|:no_smoking:",
            "&#x2757|exclamation mark|:exclamation:",
            "&#x2755|white exclamation mark|:grey_exclamation:",
            "&#x2753|question mark|:question:",
            "&#x2754|white question mark|:grey_question:",
            "&#x203C|double exclamation mark|:bangbang:",
            "&#x2049|exclamation question mark|:interrobang:",
            "&#x1F505|dim button|:low_brightness:",
            "&#x1F506|bright button|:hight_brightness:",
            "&#x303D|part alternation mark|:part_alternation_mark:",
            "&#x26A0|warning|:warning:",
            "&#x1F6B8|children crossing|:children_crossing:",
            "&#x1F531|trident emblem|:trident:",
            "&#x269C|fleur-de-lis|:fleur-de-lis:",
            "&#x1F530|Japanese symbol for beginner|:beginner:",
            "&#x267B|recycling symbol|:recycle:",
            "&#x2705|check mark button|:white_check_mark:",
            "&#x1F22F|Japanese reserved button|:u6307:",
            "&#x1F4B9|chart increasing with yen|:chart:",
            "&#x2747|sparkle|:sparkle:",
            "&#x2733|eight-spoked asterisk|eight_spoked_asterisk:",
            "&#x274E|cross mark button|:negative_squared_cross_mark:",
            "&#x24C2|circled M|:m:",
            "&#x1F300|cyclone|:cyclone:",
            "&#x1F4A4|zzz|:zzz:",
            "&#x1F3E7|ATM sign|:atm:",
            "&#x1F6BE|water closet|:wc:",
            "&#x267F|wheelchair symbol|:wheelchair:",
            "&#x1F17F|P button|:parking:",
            "&#x1F233|Japanese vacancy button|:u7a7a:",
            "&#x1F202|Japanese service charge button|:sa:",
            "&#x1F6C2|passport control|:passport_control:",
            "&#x1F6C3|customs|:customs:",
            "&#x1F6C4|baggage claim|:baggage_claim:",
            "&#x1F6C5|left luggage|:left_luggage:",
            "&#x1F6B0|potable water|:potable_water:",
            "&#x1F6B9|men’s room|:mens:",
            "&#x1F6BA|women’s room|:womens:",
            "&#x1F6BC|baby symbol|:baby_symbol:",
            "&#x1F6BB|restroom|:restroom:",
            "&#x1F6AE|litter in bin sign|:put_litter_in_its_place:",
            "&#x1F3A6|cinema|:cinema:",
            "&#x1F4F6|antenna bars|:signal_strength:",
            "&#x1F201|Japanese here button|:koko:",
            "&#x1F523|input symbols|:symbols:",
            "&#x2139|information|:information_source:",
            "&#x1F524|input latin letters|:abc:",
            "&#x1F521|input latin lowercase|:abcd:",
            "&#x1F520|input latin uppercase|:capital_abcd:",
            "&#x1F196|NG button|:ng:",
            "&#x1F197|OK button|:ok:",
            "&#x1F199| UP button|:up:",
            "&#x1F192|COOL button|:cool:",
            "&#x1F195|NEW button|:new:",
            "&#x1F193|FREE button|:free:",
            "&#x1F522|input numbers|:1234:",
            "&#x25B6|play button|:arrow_forward:",
            "&#x23F8|pause button|:double_vertical_bar:",
            "&#x23EF|play or pause button|:play_pause:",
            "&#x23F9|stop button|:stop_button:",
            "&#x23FA|record button|:record_button:",
            "&#x23ED|next track button|:next_track:",
            "&#x23EE|last track button|:previous_track:",
            "&#x23E9|fast-forward button|:fast_forward:",
            "&#x23EA|fast reverse button|:rewind:",
            "&#x23EB|fast up button|:arrow_double_up:",
            "&#x23EC|fast down button|:arrow_double_down:",
            "&#x25C0|reverse button|:arrow_backward:",
            "&#x1F53C|upwards button|:arrow_up_small:",
            "&#x1F53D|downwards button|:arrow_down_small:",
            "&#x27A1|right arrow|:arrow_right:",
            "&#x2B05|left arrow|:arrow_left:",
            "&#x2B06|up arrow|:arrow_up:",
            "&#x2B07|down arrow|:arrow_down:",
            "&#x2197|up-right arrow|:arrow_upper_right:",
            "&#x2198|down-right arrow|:arrow_lower_right:",
            "&#x2199|down-left arrow|:arrow_lower_left:",
            "&#x2196|up-left arrow|:arrow_upper_left:",
            "&#x2195|up-down arrow|:arrow_up_down:",
            "&#x2194|left-right arrow|:left_right_arrow:",
            "&#x21AA|left arrow curving right|:arrow_right_hook:",
            "&#x21A9|right arrow curving left|:leftwards_arrow_with_hook:",
            "&#x2934|right arrow curving up|:arrow_heading_up:",
            "&#x2935|right arrow curving down|:arrow_heading_down:",
            "&#x1F500|shuffle tracks button|:twisted_rightwards_arrows:",
            "&#x1F501|repeat button|:repeat:",
            "&#x1F502|repeat single button|:repeat_one:",
            "&#x1F504|counterclockwise arrows button|:arrows_counterclockwise:",
            "&#x1F503|clockwise vertical arrows|:arrows_clockwise:",
            "&#x1F3B5|musical note|:musical_note:",
            "&#x1F3B6|musical notes|:notes:",
            "&#x2795|plus|:heavy_plus_sign:",
            "&#x2796|minus|:heavy_minus_sign:",
            "&#x2797|divide|:heavy_devision_sign:",
            "&#x2716|multiply|:heavy_multiplication_x:",
            "&#x1F4B2|heavy dollar sign|:heavy_dollar_sign:",
            "&#x1F4B1|currency exchange|:currency_exchange:",
            "&#x2122|trade mark|:tm:",
            "&#x00A9|copyright|:copyright:",
            "&#x00AE|registered|:registered:",
            "&#x1F51A|END arrow|:end:",
            "&#x1F519|BACK arrow|:back:",
            "&#x1F51B|ON! arrow|:on:",
            "&#x1F51D|TOP arrow|:top:",
            "&#x1F51C|SOON arrow|:soon:",
            "&#x3030|wavy dash|:wavy_dash:",
            "&#x27B0|curly loop|:curly_loop:",
            "&#x27BF|double curly loop|:loop:",
            "&#x2714|check mark|:heavy_check_mark:",
            "&#x2611|check box with check|:ballot_box_with_check:",
            "&#x1F518|radio button|:radio_button:",
            "&#x1F535|blue circle|:blue_circle:",
            "&#x26AB|black circle|:black_circle:",
            "&#x26AA|white circle|:white_circle:",
            "&#x1F53A|red triangle pointed up|:small_red_triangle:",
            "&#x1F53B|red triangle pointed down|:small_red_triangle_down:",
            "&#x1F538|small orange diamond|:small_orange_diamond:",
            "&#x1F539|small blue diamond|:small_blue_diamond:",
            "&#x1F536|large orange diamond|:large_orange_diamond:",
            "&#x1F537|large blue diamond|:large_blue_diamond:",
            "&#x1F533|white square button|:white_square_button:",
            "&#x1F532|black square button|:black_square_button:",
            "&#x25AA|black small square|:black_small_square:",
            "&#x25AB|white small square|:white_small_square:",
            "&#x25FE|black medium-small square|:black_medium_small_square:",
            "&#x25FD|white medium-small square|:white_medium_small_square:",
            "&#x25FC|black medium square|:black_medium_square:",
            "&#x25FB|white medium square|:white_medium_square:",
            "&#x2B1B|black large square|:black_large_square:",
            "&#x2B1C|white large square|:white_large_square:",
            "&#x1F508|speaker low volume|:speaker:",
            "&#x1F507|muted speaker|:mute:",
            "&#x1F509|speaker medium volume|:sound:",
            "&#x1F50A|speaker high volume|:loud_sound:",
            "&#x1F514|bell|:bell:",
            "&#x1F515|bell with slash|:no_bell:",
            "&#x1F4E3|megaphone|:mega:",
            "&#x1F4E2|speech balloon|:speech_balloon:",
            "&#x1F4AD|thought balloon|:thought_balloon:",
            "&#x1F5EF|right anger bubble|:right_anger_bubble:",
            "&#x2660|spade suit|:spades:",
            "&#x2663|club suit|:clubs:",
            "&#x2665|heart suit|:hearts:",
            "&#x2666|diamond suit|:diamonds:",
            "&#x1F0CF|joker|:black_joker:",
            "&#x1F3B4|flower playing cards|:flower_playing_cards:",
            "&#x1F004|mahjong red dragon|:mahjong:",
            "&#x1F550|one o’clock|:clock1:",
            "&#x1F551|two o’clock|:clock2:",
            "&#x1F552|three o’clock|:clock3:",
            "&#x1F553|four o’clock|:clock4:",
            "&#x1F554|five o’clock|:clock5:",
            "&#x1F555|six o’clock|:clock6:",
            "&#x1F556|seven o’clock|:clock7:",
            "&#x1F557|eight o’clock|:clock8:",
            "&#x1F558|nine o’clock|:clock9:",
            "&#x1F559|ten o’clock|:clock10:",
            "&#x1F55A|eleven o’clock|:clock11:",
            "&#x1F55B|twelve o’clock|:clock12:",
            "&#x1F55C|one-thirty|:clock130:",
            "&#x1F55D|two-thirty|:clock230:",
            "&#x1F55E|three-thirty|:clock330:",
            "&#x1F55F|four-thirty|:clock430:",
            "&#x1F560|five-thirty|:clock530:",
            "&#x1F561|six-thirty|:clock630:",
            "&#x1F562|seven-thirty|:clock730:",
            "&#x1F563|eight-thirty|:clock830:",
            "&#x1F564|nine-thirty|:clock930:",
            "&#x1F565|ten-thirty|:clock1030:",
            "&#x1F566|eleven-thirty|:clock1130:",
            "&#x1F567|twelve-thirty|:clock1230:"
        ]
    };

    var plainList = [
        0x00A9,
        0x00AE,
        0x1F004,
        0x1F0CF,
        0x1F170,
        0x1F171,
        0x1F17E,
        0x1F17F,
        0x1F18E,
        0x1F191,
        0x1F192,
        0x1F193,
        0x1F194,
        0x1F195,
        0x1F196,
        0x1F197,
        0x1F198,
        0x1F199,
        0x1F19A,
        0x1F201,
        0x1F202,
        0x1F21A,
        0x1F22F,
        0x1F232,
        0x1F233,
        0x1F234,
        0x1F235,
        0x1F236,
        0x1F237,
        0x1F238,
        0x1F239,
        0x1F23A,
        0x1F250,
        0x1F251,
        0x1F300,
        0x1F301,
        0x1F302,
        0x1F303,
        0x1F304,
        0x1F305,
        0x1F306,
        0x1F307,
        0x1F308,
        0x1F309,
        0x1F30A,
        0x1F30B,
        0x1F30C,
        0x1F30D,
        0x1F30E,
        0x1F30F,
        0x1F310,
        0x1F311,
        0x1F312,
        0x1F313,
        0x1F314,
        0x1F315,
        0x1F316,
        0x1F317,
        0x1F318,
        0x1F319,
        0x1F31A,
        0x1F31B,
        0x1F31C,
        0x1F31D,
        0x1F31E,
        0x1F31F,
        0x1F320,
        0x1F321,
        0x1F324,
        0x1F325,
        0x1F326,
        0x1F327,
        0x1F328,
        0x1F329,
        0x1F32A,
        0x1F32B,
        0x1F32C,
        0x1F32D,
        0x1F32E,
        0x1F32F,
        0x1F330,
        0x1F331,
        0x1F332,
        0x1F333,
        0x1F334,
        0x1F335,
        0x1F336,
        0x1F337,
        0x1F338,
        0x1F339,
        0x1F33A,
        0x1F33B,
        0x1F33C,
        0x1F33D,
        0x1F33E,
        0x1F33F,
        0x1F340,
        0x1F341,
        0x1F342,
        0x1F343,
        0x1F344,
        0x1F345,
        0x1F346,
        0x1F347,
        0x1F348,
        0x1F349,
        0x1F34A,
        0x1F34B,
        0x1F34C,
        0x1F34D,
        0x1F34E,
        0x1F34F,
        0x1F350,
        0x1F351,
        0x1F352,
        0x1F353,
        0x1F354,
        0x1F355,
        0x1F356,
        0x1F357,
        0x1F358,
        0x1F359,
        0x1F35A,
        0x1F35B,
        0x1F35C,
        0x1F35D,
        0x1F35E,
        0x1F35F,
        0x1F360,
        0x1F361,
        0x1F362,
        0x1F363,
        0x1F364,
        0x1F365,
        0x1F366,
        0x1F367,
        0x1F368,
        0x1F369,
        0x1F36A,
        0x1F36B,
        0x1F36C,
        0x1F36D,
        0x1F36E,
        0x1F36F,
        0x1F370,
        0x1F371,
        0x1F372,
        0x1F373,
        0x1F374,
        0x1F375,
        0x1F376,
        0x1F377,
        0x1F378,
        0x1F379,
        0x1F37A,
        0x1F37B,
        0x1F37C,
        0x1F37D,
        0x1F37E,
        0x1F37F,
        0x1F380,
        0x1F381,
        0x1F382,
        0x1F383,
        0x1F384,
        0x1F385,
        0x1F386,
        0x1F387,
        0x1F388,
        0x1F389,
        0x1F38A,
        0x1F38B,
        0x1F38C,
        0x1F38D,
        0x1F38E,
        0x1F38F,
        0x1F390,
        0x1F391,
        0x1F392,
        0x1F393,
        0x1F396,
        0x1F397,
        0x1F399,
        0x1F39A,
        0x1F39B,
        0x1F39E,
        0x1F39F,
        0x1F3A0,
        0x1F3A1,
        0x1F3A2,
        0x1F3A3,
        0x1F3A4,
        0x1F3A5,
        0x1F3A6,
        0x1F3A7,
        0x1F3A8,
        0x1F3A9,
        0x1F3AA,
        0x1F3AB,
        0x1F3AC,
        0x1F3AD,
        0x1F3AE,
        0x1F3AF,
        0x1F3B0,
        0x1F3B1,
        0x1F3B2,
        0x1F3B3,
        0x1F3B4,
        0x1F3B5,
        0x1F3B6,
        0x1F3B7,
        0x1F3B8,
        0x1F3B9,
        0x1F3BA,
        0x1F3BB,
        0x1F3BC,
        0x1F3BD,
        0x1F3BE,
        0x1F3BF,
        0x1F3C0,
        0x1F3C1,
        0x1F3C2,
        0x1F3C3,
        0x1F3C4,
        0x1F3C5,
        0x1F3C6,
        0x1F3C7,
        0x1F3C8,
        0x1F3C9,
        0x1F3CA,
        0x1F3CB,
        0x1F3CC,
        0x1F3CD,
        0x1F3CE,
        0x1F3CF,
        0x1F3D0,
        0x1F3D1,
        0x1F3D2,
        0x1F3D3,
        0x1F3D4,
        0x1F3D5,
        0x1F3D6,
        0x1F3D7,
        0x1F3D8,
        0x1F3D9,
        0x1F3DA,
        0x1F3DB,
        0x1F3DC,
        0x1F3DD,
        0x1F3DE,
        0x1F3DF,
        0x1F3E0,
        0x1F3E1,
        0x1F3E2,
        0x1F3E3,
        0x1F3E4,
        0x1F3E5,
        0x1F3E6,
        0x1F3E7,
        0x1F3E8,
        0x1F3E9,
        0x1F3EA,
        0x1F3EB,
        0x1F3EC,
        0x1F3ED,
        0x1F3EE,
        0x1F3EF,
        0x1F3F0,
        0x1F3F3,
        0x1F3F4,
        0x1F3F5,
        0x1F3F7,
        0x1F3F8,
        0x1F3F9,
        0x1F3FA,
        0x1F400,
        0x1F401,
        0x1F402,
        0x1F403,
        0x1F404,
        0x1F405,
        0x1F406,
        0x1F407,
        0x1F408,
        0x1F409,
        0x1F40A,
        0x1F40B,
        0x1F40C,
        0x1F40D,
        0x1F40E,
        0x1F40F,
        0x1F410,
        0x1F411,
        0x1F412,
        0x1F413,
        0x1F414,
        0x1F415,
        0x1F416,
        0x1F417,
        0x1F418,
        0x1F419,
        0x1F41A,
        0x1F41B,
        0x1F41C,
        0x1F41D,
        0x1F41E,
        0x1F41F,
        0x1F420,
        0x1F421,
        0x1F422,
        0x1F423,
        0x1F424,
        0x1F425,
        0x1F426,
        0x1F427,
        0x1F428,
        0x1F429,
        0x1F42A,
        0x1F42B,
        0x1F42C,
        0x1F42D,
        0x1F42E,
        0x1F42F,
        0x1F430,
        0x1F431,
        0x1F432,
        0x1F433,
        0x1F434,
        0x1F435,
        0x1F436,
        0x1F437,
        0x1F438,
        0x1F439,
        0x1F43A,
        0x1F43B,
        0x1F43C,
        0x1F43D,
        0x1F43E,
        0x1F43F,
        0x1F440,
        0x1F441,
        0x1F442,
        0x1F443,
        0x1F444,
        0x1F445,
        0x1F446,
        0x1F447,
        0x1F448,
        0x1F449,
        0x1F44A,
        0x1F44B,
        0x1F44C,
        0x1F44D,
        0x1F44E,
        0x1F44F,
        0x1F450,
        0x1F451,
        0x1F452,
        0x1F453,
        0x1F454,
        0x1F455,
        0x1F456,
        0x1F457,
        0x1F458,
        0x1F459,
        0x1F45A,
        0x1F45B,
        0x1F45C,
        0x1F45D,
        0x1F45E,
        0x1F45F,
        0x1F460,
        0x1F461,
        0x1F462,
        0x1F463,
        0x1F464,
        0x1F465,
        0x1F466,
        0x1F467,
        0x1F468,
        0x1F469,
        0x1F46A,
        0x1F46B,
        0x1F46C,
        0x1F46D,
        0x1F46E,
        0x1F46F,
        0x1F470,
        0x1F471,
        0x1F472,
        0x1F473,
        0x1F474,
        0x1F475,
        0x1F476,
        0x1F477,
        0x1F478,
        0x1F479,
        0x1F47A,
        0x1F47B,
        0x1F47C,
        0x1F47D,
        0x1F47E,
        0x1F47F,
        0x1F480,
        0x1F481,
        0x1F482,
        0x1F483,
        0x1F484,
        0x1F485,
        0x1F486,
        0x1F487,
        0x1F488,
        0x1F489,
        0x1F48A,
        0x1F48B,
        0x1F48C,
        0x1F48D,
        0x1F48E,
        0x1F48F,
        0x1F490,
        0x1F491,
        0x1F492,
        0x1F493,
        0x1F494,
        0x1F495,
        0x1F496,
        0x1F497,
        0x1F498,
        0x1F499,
        0x1F49A,
        0x1F49B,
        0x1F49C,
        0x1F49D,
        0x1F49E,
        0x1F49F,
        0x1F4A0,
        0x1F4A1,
        0x1F4A2,
        0x1F4A3,
        0x1F4A4,
        0x1F4A5,
        0x1F4A6,
        0x1F4A7,
        0x1F4A8,
        0x1F4A9,
        0x1F4AA,
        0x1F4AB,
        0x1F4AC,
        0x1F4AD,
        0x1F4AE,
        0x1F4AF,
        0x1F4B0,
        0x1F4B1,
        0x1F4B2,
        0x1F4B3,
        0x1F4B4,
        0x1F4B5,
        0x1F4B6,
        0x1F4B7,
        0x1F4B8,
        0x1F4B9,
        0x1F4BA,
        0x1F4BB,
        0x1F4BC,
        0x1F4BD,
        0x1F4BE,
        0x1F4BF,
        0x1F4C0,
        0x1F4C1,
        0x1F4C2,
        0x1F4C3,
        0x1F4C4,
        0x1F4C5,
        0x1F4C6,
        0x1F4C7,
        0x1F4C8,
        0x1F4C9,
        0x1F4CA,
        0x1F4CB,
        0x1F4CC,
        0x1F4CD,
        0x1F4CE,
        0x1F4CF,
        0x1F4D0,
        0x1F4D1,
        0x1F4D2,
        0x1F4D3,
        0x1F4D4,
        0x1F4D5,
        0x1F4D6,
        0x1F4D7,
        0x1F4D8,
        0x1F4D9,
        0x1F4DA,
        0x1F4DB,
        0x1F4DC,
        0x1F4DD,
        0x1F4DE,
        0x1F4DF,
        0x1F4E0,
        0x1F4E1,
        0x1F4E2,
        0x1F4E3,
        0x1F4E4,
        0x1F4E5,
        0x1F4E6,
        0x1F4E7,
        0x1F4E8,
        0x1F4E9,
        0x1F4EA,
        0x1F4EB,
        0x1F4EC,
        0x1F4ED,
        0x1F4EE,
        0x1F4EF,
        0x1F4F0,
        0x1F4F1,
        0x1F4F2,
        0x1F4F3,
        0x1F4F4,
        0x1F4F5,
        0x1F4F6,
        0x1F4F7,
        0x1F4F8,
        0x1F4F9,
        0x1F4FA,
        0x1F4FB,
        0x1F4FC,
        0x1F4FD,
        0x1F4FF,
        0x1F500,
        0x1F501,
        0x1F502,
        0x1F503,
        0x1F504,
        0x1F505,
        0x1F506,
        0x1F507,
        0x1F508,
        0x1F509,
        0x1F50A,
        0x1F50B,
        0x1F50C,
        0x1F50D,
        0x1F50E,
        0x1F50F,
        0x1F510,
        0x1F511,
        0x1F512,
        0x1F513,
        0x1F514,
        0x1F515,
        0x1F516,
        0x1F517,
        0x1F518,
        0x1F519,
        0x1F51A,
        0x1F51B,
        0x1F51C,
        0x1F51D,
        0x1F51E,
        0x1F520,
        0x1F521,
        0x1F522,
        0x1F523,
        0x1F524,
        0x1F525,
        0x1F526,
        0x1F527,
        0x1F528,
        0x1F529,
        0x1F52A,
        0x1F52B,
        0x1F52C,
        0x1F52D,
        0x1F52E,
        0x1F52F,
        0x1F530,
        0x1F531,
        0x1F532,
        0x1F533,
        0x1F534,
        0x1F535,
        0x1F536,
        0x1F537,
        0x1F538,
        0x1F539,
        0x1F53A,
        0x1F53B,
        0x1F53C,
        0x1F53D,
        0x1F549,
        0x1F54A,
        0x1F54B,
        0x1F54C,
        0x1F54D,
        0x1F54E,
        0x1F550,
        0x1F551,
        0x1F552,
        0x1F553,
        0x1F554,
        0x1F555,
        0x1F556,
        0x1F557,
        0x1F558,
        0x1F559,
        0x1F55A,
        0x1F55B,
        0x1F55C,
        0x1F55D,
        0x1F55E,
        0x1F55F,
        0x1F560,
        0x1F561,
        0x1F562,
        0x1F563,
        0x1F564,
        0x1F565,
        0x1F566,
        0x1F567,
        0x1F56F,
        0x1F570,
        0x1F573,
        0x1F574,
        0x1F575,
        0x1F576,
        0x1F577,
        0x1F578,
        0x1F579,
        0x1F587,
        0x1F58A,
        0x1F58B,
        0x1F58C,
        0x1F58D,
        0x1F590,
        0x1F595,
        0x1F596,
        0x1F5A5,
        0x1F5A8,
        0x1F5B1,
        0x1F5B2,
        0x1F5BC,
        0x1F5C2,
        0x1F5C3,
        0x1F5C4,
        0x1F5D1,
        0x1F5D2,
        0x1F5D3,
        0x1F5DC,
        0x1F5DD,
        0x1F5DE,
        0x1F5E1,
        0x1F5E3,
        0x1F5E8,
        0x1F5EF,
        0x1F5F3,
        0x1F5FA,
        0x1F5FB,
        0x1F5FC,
        0x1F5FD,
        0x1F5FE,
        0x1F5FF,
        0x1F600,
        0x1F601,
        0x1F602,
        0x1F603,
        0x1F604,
        0x1F605,
        0x1F606,
        0x1F607,
        0x1F608,
        0x1F609,
        0x1F60A,
        0x1F60B,
        0x1F60C,
        0x1F60D,
        0x1F60E,
        0x1F60F,
        0x1F610,
        0x1F611,
        0x1F612,
        0x1F613,
        0x1F614,
        0x1F615,
        0x1F616,
        0x1F617,
        0x1F618,
        0x1F619,
        0x1F61A,
        0x1F61B,
        0x1F61C,
        0x1F61D,
        0x1F61E,
        0x1F61F,
        0x1F620,
        0x1F621,
        0x1F622,
        0x1F623,
        0x1F624,
        0x1F625,
        0x1F626,
        0x1F627,
        0x1F628,
        0x1F629,
        0x1F62A,
        0x1F62B,
        0x1F62C,
        0x1F62D,
        0x1F62E,
        0x1F62F,
        0x1F630,
        0x1F631,
        0x1F632,
        0x1F633,
        0x1F634,
        0x1F635,
        0x1F636,
        0x1F637,
        0x1F638,
        0x1F639,
        0x1F63A,
        0x1F63B,
        0x1F63C,
        0x1F63D,
        0x1F63E,
        0x1F63F,
        0x1F640,
        0x1F641,
        0x1F642,
        0x1F643,
        0x1F644,
        0x1f645,
        0x1F646,
        0x1F647,
        0x1F648,
        0x1F649,
        0x1F64A,
        0x1F64B,
        0x1F64C,
        0x1F64D,
        0x1F64E,
        0x1F64F,
        0x1F680,
        0x1F681,
        0x1F682,
        0x1F683,
        0x1F684,
        0x1F685,
        0x1F686,
        0x1F687,
        0x1F688,
        0x1F689,
        0x1F68A,
        0x1F68B,
        0x1F68C,
        0x1F68D,
        0x1F68E,
        0x1F68F,
        0x1F690,
        0x1F691,
        0x1F692,
        0x1F693,
        0x1F694,
        0x1F695,
        0x1F696,
        0x1F697,
        0x1F698,
        0x1F699,
        0x1F69A,
        0x1F69B,
        0x1F69C,
        0x1F69D,
        0x1F69E,
        0x1F69F,
        0x1F6A0,
        0x1F6A1,
        0x1F6A2,
        0x1F6A3,
        0x1F6A4,
        0x1F6A5,
        0x1F6A6,
        0x1F6A7,
        0x1F6A8,
        0x1F6A9,
        0x1F6AA,
        0x1F6AB,
        0x1F6AC,
        0x1F6AD,
        0x1F6AE,
        0x1F6AF,
        0x1F6B0,
        0x1F6B1,
        0x1F6B2,
        0x1F6B3,
        0x1F6B4,
        0x1F6B5,
        0x1F6B6,
        0x1F6B7,
        0x1F6B8,
        0x1F6B9,
        0x1F6BA,
        0x1F6BB,
        0x1F6BC,
        0x1F6BD,
        0x1F6BE,
        0x1F6BF,
        0x1F6C0,
        0x1F6C1,
        0x1F6C2,
        0x1F6C3,
        0x1F6C4,
        0x1F6C5,
        0x1F6CB,
        0x1F6CC,
        0x1F6CD,
        0x1F6CE,
        0x1F6CF,
        0x1F6D0,
        0x1F6E0,
        0x1F6E1,
        0x1F6E2,
        0x1F6E3,
        0x1F6E4,
        0x1F6E5,
        0x1F6E9,
        0x1F6EB,
        0x1F6EC,
        0x1F6F0,
        0x1F6F3,
        0x1F910,
        0x1F911,
        0x1F912,
        0x1F913,
        0x1F914,
        0x1F915,
        0x1f916,
        0x1F917,
        0x1F918,
        0x1F980,
        0x1F981,
        0x1F982,
        0x1F983,
        0x1F984,
        0x1F9C0,
        0x203C,
        0x2049,
        0x2122,
        0x2139,
        0x2194,
        0x2195,
        0x2196,
        0x2197,
        0x2198,
        0x2199,
        0x21A9,
        0x21AA,
        0x231A,
        0x231B,
        0x2328,
        0x23E9,
        0x23EA,
        0x23EB,
        0x23EC,
        0x23ED,
        0x23EE,
        0x23EF,
        0x23F0,
        0x23F1,
        0x23F2,
        0x23F3,
        0x23F8,
        0x23F9,
        0x23FA,
        0x24C2,
        0x25AA,
        0x25AB,
        0x25B6,
        0x25C0,
        0x25FB,
        0x25FC,
        0x25FD,
        0x25FE,
        0x2600,
        0x2601,
        0x2602,
        0x2603,
        0x2604,
        0x260E,
        0x2611,
        0x2614,
        0x2615,
        0x2618,
        0x261D,
        0x2620,
        0x2622,
        0x2623,
        0x2626,
        0x262A,
        0x262E,
        0x262F,
        0x2638,
        0x2639,
        0x263A,
        0x2648,
        0x2649,
        0x264A,
        0x264B,
        0x264C,
        0x264D,
        0x264E,
        0x264F,
        0x2650,
        0x2651,
        0x2652,
        0x2653,
        0x2660,
        0x2663,
        0x2665,
        0x2666,
        0x2668,
        0x267B,
        0x267F,
        0x2692,
        0x2693,
        0x2694,
        0x2696,
        0x2697,
        0x2699,
        0x269B,
        0x269C,
        0x26A0,
        0x26A1,
        0x26AA,
        0x26AB,
        0x26B0,
        0x26B1,
        0x26BD,
        0x26BE,
        0x26C4,
        0x26C5,
        0x26C8,
        0x26CE,
        0x26CF,
        0x26D1,
        0x26D3,
        0x26D4,
        0x26E9,
        0x26EA,
        0x26F0,
        0x26F1,
        0x26F2,
        0x26F3,
        0x26F4,
        0x26F5,
        0x26F7,
        0x26F8,
        0x26F9,
        0x26FA,
        0x26FD,
        0x2702,
        0x2705,
        0x2708,
        0x2709,
        0x270A,
        0x270B,
        0x270C,
        0x270D,
        0x270F,
        0x2712,
        0x2714,
        0x2716,
        0x271D,
        0x2721,
        0x2728,
        0x2733,
        0x2734,
        0x2744,
        0x2747,
        0x274C,
        0x274E,
        0x2753,
        0x2754,
        0x2755,
        0x2757,
        0x2763,
        0x2764,
        0x2795,
        0x2796,
        0x2797,
        0x27A1,
        0x27B0,
        0x27BF,
        0x2934,
        0x2935,
        0x2B05,
        0x2B06,
        0x2B07,
        0x2B1B,
        0x2B1C,
        0x2B50,
        0x2B55,
        0x3030,
        0x303D,
        0x3297,
        0x3299
    ];

    this.getPackage = function (numb) {
        return packageList.hasOwnProperty(numb) ? packageList[numb] : [];
    };
    
    this.getAllEmoji = function () {
        return packageList[1]
            .concat(packageList[2])
            .concat(packageList[3])
            .concat(packageList[4])
            .concat(packageList[5])
            .concat(packageList[6])
            .concat(packageList[7])
    };

    this.getPlainList = function () {
        return plainList;
    }
}










function mcUsersSmallInfo($rootScope) {
    "use strict";

    // тут храниться краткая инфа про любых пользователей - приватов и конференций. Нельзя объединить с диалогами

    var __users             = {};
    var __updateRequestPool = [];
    var __updateInProgress  = false;

    function _setInfo(info){
        if (info && (!__users[info.UIN] || (__users[info.UIN] && info.ChangeID >= __users[info.UIN].ChangeID)) && info.DisplayName){
            __users[info.UIN] = info;
        }
    }

    function _getInfo(uin) { // changeId
        if (uin !== void(0)) {
            // if (!__users[uin] || !__users[uin].DisplayName){
            //     console.log(uin + ":" + __users[uin]);
            // }

            return mcService.Marge({DisplayName: "noNameUser"}, __users[uin]);
        }
    }

    function _getUsersInfoByUins(uins, cb) {
        if (uins !== void(0)){
            /*!!!!!!!!!!!!!console.log("_getUsersInfoByUins: " + window.performance.now());/*!!!!!!!!!!!!!*/

            $rootScope.$broadcast('SendCMDToServer' , [
                mcConst._CMD_.cs_get_users_changeid_data,
                mcConst.SessionID,

                uins,

                function (resList) {
                    /*!!!!!!!!!!!!!console.log("cs_get_users_changeid_data: " + window.performance.now());/*!!!!!!!!!!!!!*/

                    _updateUsersList(resList);

                    var data = "";

                    if (uins !== "" && (mcService.isNumber(uins) || uins.indexOf(',') === -1)) {
                        data = _getInfo(uins)
                    }

                    cb && cb(data);
                }
            ]);
        }
    }

    function _updateUsersList(data) {
        if (data && mcService.isArray(data) && data.length) {
            data.forEach(function (userInfo) {
                _setInfo(userInfo);
            });
        } else
        if (data && mcService.isObject(data)) {
            Object.keys(data).forEach(function (user) {
                _setInfo(data[user]);
            });
        }
    }

    function _getOrUpdateUsers(users, cb) {
        // users - массив объектов

        function next() {
            cb && cb(users);

            if (__updateRequestPool.length){
                setTimeout(function () {
                    __updateInProgress = false;

                    _getOrUpdateUsers.apply(null, __updateRequestPool.shift());
                }, 1);
            } else {
                __updateInProgress = false
            }
        }

        if (__updateInProgress) {
            __updateRequestPool.push([users, cb]);
        } else
        
        if (users && users.length){
            var updateUsers = [];

            __updateInProgress = true;

            users.forEach(function (user) {
                var localUser = __users[user.UIN];

                if (!localUser || (localUser && localUser.ChangeID != user.ChangeID) ) {
                    updateUsers.push(user.UIN);
                } else
                if (localUser && localUser.ChangeID == user.ChangeID) {
                    user = mcService.Marge(user, localUser);
                }
            });

            if (updateUsers.length) {
                $rootScope.$broadcast('SendCMDToServer' , [
                    mcConst._CMD_.cs_get_users_changeid_data,
                    mcConst.SessionID,

                    updateUsers.join(','),

                    function (data) {
                        if (data && data.length) {
                            data.forEach(function (userInfo) {
                                if (userInfo){
                                    _setInfo(userInfo);

                                    var idx = mcService.findItemInArrayOfObj(users, userInfo.UIN, "UIN");

                                    if (idx !== -1){
                                        users[idx] = mcService.Marge(users[idx], userInfo);
                                    } else
                                    if (window.loadProjectType === window.MC_DEVELOP) {
                                        console.warn("_getOrUpdateUsers: cs_get_users_changeid_data");
                                        console.warn(userInfo);
                                        console.warn(users);
                                    }
                                }
                            });
                        }

                        next(); // обновили инфу
                    }
               ]);
            } else {
                next(); // никого из списка не нужно обновлять
            }
        } else {
            next(); // где бля список?
        }
    }

    var msg = window._messages_.usersSmallInfo = {
        getUserSmallInfo        : "getUserSmallInfo",
        setUserSmallInfo        : "setUserSmallInfo",
        updateUsersSmallInfo    : "updateUsersSmallInfo",
        getInfoByUserUINs       : "getInfoByUserUINs",
        getAllUsersInfo         : "getAllUsersInfo",
        setAllUsersInfo         : "setAllUsersInfo",
        // setChangeIdForUser      : "setChangeIdForUser"
    };

    // $rootScope.$on(msg.setChangeIdForUser, function(e, args){
    //     var uin = args[0];
    //     var id  = args[1];
        //
        // var uin  = args[0];
        // var user = args[1];
        //
        // if (usersChangeID[uin] && usersChangeID[uin] !== user.ChangeID && currentUIN == uin){
        //         $rootScope.$broadcast(window._messages_.dialogsList.getUserProfile, [_user.UIN, _user.ChangeID, function (user) {
            // fillPrivateInfo(user, uin);
            //
            // $scope.drawFoto(user.Photo);
            // }]);
        // }
        //
        // usersChangeID[uin] = user.ChangeID;
        //
        // $rootScope.$broadcast(window._messages_.dialogsCtrl.updateDialogInfo, [uin, {
        //     DisplayName : user.DISPLAYNAME
        // }]);
    // });

    $rootScope.$on(msg.getInfoByUserUINs, function (e, args) {
        _getUsersInfoByUins.apply(null, args);
    });

    $rootScope.$on(msg.setUserSmallInfo, function (e, args) {
        _setInfo.apply(null, args);
    });

    $rootScope.$on(msg.getUserSmallInfo, function (e, args) {
        return _getInfo.apply(null, args);
    });

    $rootScope.$on(msg.updateUsersSmallInfo, function (e, args) {
        _getOrUpdateUsers.apply(null, args);
    });

    $rootScope.$on(msg.getAllUsersInfo, function () {
        return mcService.Marge({}, __users);
    });

    $rootScope.$on(msg.setAllUsersInfo, function (e, args) {
        _updateUsersList.apply(null, args);
    });
}

function McFramesList(_onWheel) {
    "use strict";

    var _list = {};
    var self  = this;
    var chatTextOutput = null;
    var onWheel = _onWheel || null;

    function _show (cb) {
        self.show(this.id, cb);

        if (this.scrollOnShow){
            this.scrollOnShow();
        }
    }
    function _hide (cb) {
        self.hide(this.id, cb)
    }
    function _removeItem (el) {
        this.firstChild.removeChild(el);
    }
    function _addToFoot (el) {
        this.firstChild.appendChild(el);
    }
    function _addToHead (insertItem, tt) {
        (!tt || this.firstChild.firstChild) && this.firstChild.insertBefore(insertItem, this.firstChild.firstChild);
    }
    function _getByAttrFirst (attr) {
        return this.firstChild.firstChild ? this.firstChild.firstChild.getAttribute(attr) : null;
    }
    function _getByAttrLast (attr) {
        return this.firstChild.lastChild ? this.firstChild.lastChild.getAttribute(attr) : null;
    }
    function _getContainer () {
        return this.firstChild;
    }
    function _getLastMsg () {
        return this.firstChild.lastChild || null;
    }
    function _getFirstMsg () {
        return this.firstChild.firstChild || null;
    }
    function _removeMsg (el) {
        this.firstChild.removeChild(el);
    }
    function _getScrollH () {
        // return this.firstChild.scrollHeight;
        return this.scrollHeight;
    }
    function _mcDropHandle (foo) {
        if (!this.ondrop) {
            this.ondrop = foo;
        }
    }

    // ======================================================

    function createFrame(frame) {
        var element = null;

        if (window.mcService.isHtmlElement(frame) && frame.id){
            element = frame;
        } else

        if (window.mcService.isString(frame)){
            element = document.getElementById(frame) || document.createElement('div');

            element.className    = "paddingLeft7 scrollMenu hiddenScrollMenu "; // framePaddingР30
            element.id           = frame;
            element.chatType     = frame.split('-')[0];
            element.chatID       = frame.split('-')[1];
            element.style.height = "100%";
            element.innerHTML    = "<div class='_innerContainer'></div>";
        } else {
            console.error("Can't create or find frame: ");
            console.error(frame);
        }

        if (element){
            element.show           = _show;
            element.hide           = _hide;
            element.removeItem     = _removeItem;
            element.addToFoot      = _addToFoot;
            element.addToHead      = _addToHead;
            element.getByAttrFirst = _getByAttrFirst;
            element.getByAttrLast  = _getByAttrLast;
            element.getContainer   = _getContainer;
            element.getLastMsg     = _getLastMsg;
            element.getFirstMsg    = _getFirstMsg;
            element.removeMsg      = _removeMsg;
            element.getScrollH     = _getScrollH;
            element.mcDropHandle   = _mcDropHandle;
            element.ondragleave    = element.ondragover = function() { return false };
            
            if (onWheel) {
                mcService.addEvent(element, "wheel", function (e) {
                    onWheel(element, e);
                });

                mcService.addEvent(element, "mouseup", function (e) {
                    e.button === 0 && onWheel(element, e);
                });
            } else {
                console.log("Create frame without history: " + frame);
                
                window.___native_console.trace();
            }

            _list[element.id] = element;

            chatTextOutput.appendChild(element);

            mcService.addEvent(element.firstChild, "resize", function () {
                console.log(frame + " " + element.firstChild.offsetWight + ": " + element.firstChild.scrollTop + ": " +element.firstChild.scrollHeight);
            });
        }

        // if (progress){
        //     var progressWrapper = document.createElement('div');
        //
        //     progressWrapper.id             = frame + "-progressBar";
        //     progressWrapper.style.position = "absolute";
        //     progressWrapper.style.zIndex   = 1;
        //     progressWrapper.style.bottom   = 0; // chatTextOutput.offsetTop   + "px";
        //     progressWrapper.style.width    = "auto";//chatTextOutput.offsetWidth + "px";
        //     progressWrapper.style.height   = "auto";
        //
        //     element.appendChild(progressWrapper);
        // }

        return element;
    }

    function get (frame, noCreate) {
        return _list[frame] ? _list[frame] : (!noCreate && createFrame(frame));
    }

    // ======================================================

    this.get    = get;
    this.create = createFrame;

    this.clear  = function (_frame){
        var frame = get(_frame, true);

        if (frame) {
            frame.innerHTML = "";
        }
    };

    this.remove = function (_frame) {
        // var frame = get(isNaN(parseInt(_frame)) ? _frame : 'UID-' + _frame);
        var frame = get(_frame, true);

        if (frame) {
            if (_list[frame.id]) {
                delete _list[frame.id];
            }

            frame.parentElement.removeChild(frame);
        }
    };

    this.show = function (frameID, cb) {
        if (_list[frameID]) {
            var frame = get(frameID, true);

            if (frame.style.display !== "block") {
                frame.style.display = 'block';

                // console.log("show: " + frame.scrollTop + "-" + frame.scrollHeight + "-" + _list[frameID].restoreScroll);

                // frame.scrollTop = _list[frameID].restoreScroll;
                frame.scrollTop = frame.scrollHeight - _list[frameID].restoreScroll -  frame.offsetHeight;

                cb && cb(frameID);
            }

            setTimeout(function () {
                var badges = frame.getElementsByClassName("msgBadge-" + frame.chatID);

                for (var i = 0; i < badges.length; i++){
                    try {
                        badges[i].parentElement.removeChild(badges[i]);
                    } catch (e) {}
                }
            }, 1000 * 3);
        }
    };

    this.hide = function (frameID, cb) {
        if (_list[frameID]) {
            var frame = get(frameID,  true);

            if (frame.style.display !== "none"){
                _list[frameID].restoreScroll = frame.scrollHeight - (frame.scrollTop + frame.offsetHeight); // + frame.scrollHeight;

                // console.log("hide: " + frame.scrollTop + "-" + frame.scrollHeight + "-" + _list[frameID].restoreScroll);

                frame.style.display = 'none';

                cb && cb(frameID);
            }
        }
    };

    this.hasFrame = function (frame) {
        return _list.hasOwnProperty(frame);
    };

    this.initView = function (view) {
        chatTextOutput = view;
    };
}
// Created by Gifer 01.02.2023

function mcAvatars($rootScope){
    var TABLE_NAME   = "Avatars";
    var __listByUINs = {};
    var sheetList    = {} ;

    sheetList[mcConst.avatars.SMALL]  = document.createElement('style');
    sheetList[mcConst.avatars.MEDIUM] = document.createElement('style');
    sheetList[mcConst.avatars.LARGE]  = document.createElement('style');

    document.getElementsByTagName("head")[0].appendChild(sheetList[mcConst.avatars.SMALL]);
    document.getElementsByTagName("head")[0].appendChild(sheetList[mcConst.avatars.MEDIUM]);
    document.getElementsByTagName("head")[0].appendChild(sheetList[mcConst.avatars.LARGE]);

    function loadAllAvatars() {
        if ($rootScope.dbConnect){
            $rootScope.dbConnect
            .select({
                from: TABLE_NAME
            })
            .then(function(_avatars) {
                setAvatarsInfo(_avatars);

                console.log('Avatars loaded: ' + (_avatars && _avatars.length));
            })
        }
    }

    function setAvatarsInfo(data, save) {
        var toSave = [];

        if (data && mcService.isArray(data) && data.length) {
            data.forEach(function (avatarInfo) {
                __listByUINs [avatarInfo.UIN]   = avatarInfo;
                __listByUINs [avatarInfo.UIN].showed = {};
                // __listByCRC32[avatarInfo.CRC32] = avatarInfo.UIN;

                if (save && avatarInfo.CRC32){
                    toSave.push(avatarInfo);
                }
            });

            if (save && $rootScope.dbConnect){
                $rootScope.dbConnect.insert({
                    into  : TABLE_NAME,
                    values: toSave,
                    upsert: true
                }).catch(function(err) {
                    console.log(err.message);
                });
            }
        }
    }

    function renderAvatars(_type, users) {
        var className = "ChatAvatar";

        switch (_type){
            case mcConst.avatars.SMALL : className = ".small"  + className; break;
            case mcConst.avatars.MEDIUM: className = ".medium" + className; break;
            case mcConst.avatars.LARGE : className = ".large"  + className; break;
        }

        var styleForUsers = {};

        // find and replace full name like ".smallChatAvatar123" to UIN - "123", other type was not changed
        sheetList[_type].childNodes.forEach(function (_style) {
            if (_style && _style.data) {
                var userUIN = mcService.copy(_style.data, 0, _style.data.indexOf(" ")).replace(className, ""); // get UIN from class name
                
                styleForUsers[userUIN] = _style.data;
            }
        });

        // generate list and update existing styles
        (mcService.isObject(users) ? Object.keys(users) : users).forEach(function (_user){
            var user = mcService.isString(_user) ? users[_user] : _user;

            if (user.CRC32){
                user.showed[_type] = true;

                styleForUsers[user.UIN.toString()] = className + user.UIN + " {opacity: .85;color: transparent;text-shadow: none;background-size: contain;background-position: center;" +
                    "background-image: url(data:image/jpg;base64," + user.Data + ");}"
            }
        });

        // replace all styles for newest styles
        sheetList[_type].innerHTML = (Object.keys(styleForUsers).map(function (item){
            return styleForUsers[item]
        }).join('\n'));
    }

    function addAvatars(_avatars, save, _type) {
        if (_avatars && _avatars.length){
            setAvatarsInfo(_avatars, save);
            renderAvatars(_type || mcConst.avatars.MEDIUM, __listByUINs);
        }
    }

    function hasAvatar(uin) {
        return __listByUINs.hasOwnProperty(uin);
    }

    function showAvatars(uins, _type, _reload) {
        var loadUins = [];
        var showUins = [];

        if (!_type) {
            _type = mcConst.avatars.MEDIUM;
        }

        if (uins) {
            if (mcService.isNumber(uins)) uins = uins.toString();

            (mcService.isArray(uins) ? uins : uins.split(',')).forEach(function (_uin) {
                var user = __listByUINs[_uin];

                _uin = parseInt(_uin);

                if (isNaN(_uin) || _uin === 0){
                    // if is not number or user elisa - exit
                } else
                if (hasAvatar(_uin)) {
                    var baseCrc32 = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUsersAvatarCRC, [_uin]);

                    if (baseCrc32 && baseCrc32 === user.CRC32 ) {
                        if (!user.showed[_type]) {
                            showUins.push(_uin);
                        }
                    } else {
                        loadUins.push(_uin);
                    }
                } else {
                    loadUins.push(_uin);
                }
            });

            if (loadUins.length){
                $rootScope.$broadcast('SendCMDToServer', [
                    mcConst._CMD_.cs_get_user_avatars,
                    mcConst.SessionID,

                    loadUins.join(','),
                    _type,
                    _type,

                    function (data) {
                        addAvatars(data, true, _type);
                    }
                ]);
            }

            showUins.forEach(function (_uin, idx) {
                if (hasAvatar(_uin)) showUins[idx] = __listByUINs[_uin];
            });

            if (showUins.length) {
                renderAvatars(_type, showUins);
            }
        }
    }

    var msg = window._messages_.avatarsManager = {
        loadAllAvatars  : 'loadAllAvatars',
        // addAvatars      : 'addAvatars',
        showAvatars     : 'showAvatars'
    };

    $rootScope.$on(msg.loadAllAvatars, loadAllAvatars);

    // $rootScope.$on(msg.addAvatars, function (e, args){
    //     addAvatars(args[0]);
    // });

    $rootScope.$on(msg.showAvatars, function (e, args){
        showAvatars(args[0], args[1]);
    });
}
function initLogin($scope){
    "use strict";

    var login = null;

    if ($$($scope.Name)) {
        $$($scope.Name).destructor();
    }

    function allOk() {
        return true;
    }

    var loginBody = {
        id   : 'loginForm',
        view : 'form',
        rules:{
            "loginField"    : $scope.fields && !$scope.oldLink ? ($scope.fields.indexOf('name')  === -1 ? allOk : webix.rules.isNotEmpty)        : allOk,
            "emailField"    : $scope.fields && !$scope.oldLink ? ($scope.fields.indexOf('email') === -1 ? allOk : mcService.isValidEmailAddress) : allOk
        },
        elements:[ { rows: [
            { id: 'selectOperator', view: "richselect", hidden: true, options: {
                    data: []
                }, on: {
                    onChange: $scope.selectUser
                }
            },

            { id: 'loginField', name: 'loginField', view: "text", value: $scope.login, required: true, hidden: $scope.oldLink ? false : ($scope.fields !== null ? $scope.fields.indexOf('name') === -1 : true),
                invalidMessage: "<span class='red h10'>" + mcService.Lang(66) + "</span>", // "66" : "Поле не может быть пустым",
                placeholder: mcService.Lang(729)}, // "729":"Ваше имя",

            { id: 'emailField', name: 'emailField', view: "text", value: $scope.email, required: true, hidden: $scope.oldLink ? false : ($scope.fields !== null? $scope.fields.indexOf('email') === -1 : true),
                invalidMessage: "<span class='red h10'>" + mcService.Lang(66) + "</span>", // "66" : "Поле не может быть пустым",
                placeholder: mcService.Lang(59), // "59" : "Введите ваш Email",
                on: {
                    onKeyPress: function (code) {
                        if (code === 13){
                            $scope.loginBtnClick();
                        }
                    }
                }
            },

            { id: 'passwordField', name: 'passwordField', view: "text", type: 'password', /*required: true,*/ keyPressTimeout: 100, hidden: true,
                invalidMessage: "<span class='red h10'>" + mcService.Lang(66) + "</span>", // "66" : "Поле не может быть пустым",
                placeholder: mcService.Lang(2), // "2" : "Пароль"
                on: {
                    onKeyPress      : $scope.keyLogin,
                    onTimedKeyPress : function() {
                        $scope.checkLangPass($scope.loginDispLang, $scope.passwordField);
                    }
                }
            },

            { height: 15},

            { id: "loginBtn", view: "button", value: mcLang(730), click: $scope.loginBtnClick} // "730":"Задать вопрос",
        ]}]
    };

    if (isMobile){
        loginBody.maxWidth = 550;
        loginBody.minWidth = 300;

        login = webix.ui({
            id       : $scope.Name,
            view     : "scrollview",
            css      : "noBGColor",
            body     : { rows: [
                {},
                loginBody,
                {}
            ]}
        });
    } else {
        if (!$$("loginLang")){
            webix.ui({
                view    : "richselect",
                id      : "loginLang",
                value   : $scope.lang,
                css     : "noBGColorAll",
                container: "languageChanger",
                options : {
                    template : "#value#",
                    data     : [
                        { id : 'en', value : '<img src="' + mcConst.imagesPath.flags + 'en32.png" align="left">&nbsp; english'},
                        { id : 'uk', value : '<img src="' + mcConst.imagesPath.flags + 'uk32.png" align="left">&nbsp; українська'},
                        { id : 'ru', value : '<img src="' + mcConst.imagesPath.flags + 'ru32.png" align="left">&nbsp; русский'}
                    ]
                },
                on : {
                    onChange: $scope.changeLang
                }
            });
        }

        login = webix.ui({
            id       : $scope.Name,
            view     : "window",
            width    : 370,
            position : "center",
            head     : false,
            body     : { rows: [
                loginBody
            ]},
            on : {
                onHide: function () {
                    if (!$scope.doLogin) { // todo: mazafaka fix  окно прячется при нажатии на ESC, баг вебикса
                        login.show();
                    }
                }
            }
        });
    }

    $scope.loginForm        = $$('loginForm');
    $scope.passwordField    = $$('passwordField');
    $scope.loginDispLang    = $$('loginDispLang');
    $scope.loginField       = $$("loginField");
    $scope.emailField       = $$("emailField");
    $scope.loginBtn         = $$("loginBtn");
    $scope.selectOperator   = $$("selectOperator");

    setTimeout(function () {
        $scope.loginField.focus();
    }, 10);

    // login.show();

    return login;
}

function loginController ($scope, $rootScope, $location){
    "use strict";

    $scope.Name = mcConst.dataModels.Login;
    $scope.remember     = false;
    $scope.serverList   = [];
    $scope.doLogin      = false;
    $scope.enableSelect = false;
    $scope.fields       = null;
    $rootScope.inFrame  = false;

    var _view         = null;

    // ======= Error Actions ==========

    $rootScope.ErrorActions[80] =
    $rootScope.ErrorActions[mcConst._CMD_.Errors.WrongPassword] = function(){ // "14"  : "Неверный пароль пользователя",
        $rootScope.loginFormNotShowed = false;

        mcService.SaveSessionSettings("", "", "", false, "");

        mcConst.LoginInfo.login   = "";
        mcConst.LoginInfo.pwd     = "";
        mcConst.LoginInfo.servPwd = "";
        mcConst.LoginInfo.rm      = false;
        mcConst.LoginInfo.Email   = "";

        // if (!_view){
        //     _view = initLogin($scope);
        //
        //     $scope.loginField.setValue("");
        // }

        $scope.show();

        $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

        webix.ready(function(){
            setTimeout(function(){
                $scope.passwordField.focus();
            }, 100);
        });
    };

    $rootScope.ErrorActions[81] = function (params) {
        $rootScope.$broadcast('ErrorMsg', [81, mcConst.ErrorText[81], params, function(){
            $rootScope.ErrorActions[14]();
        }]);
    };

    $rootScope.ErrorActions[121] = function (params) {
        $rootScope.$broadcast('ErrorMsg', [121, mcConst.ErrorText[121], params, function(){
            $rootScope.ErrorActions[14]();
        }]);
    };

    $rootScope.ErrorActions[24] = function (params) {
        $rootScope.$broadcast('ErrorMsg', [24, mcConst.ErrorText[24], params, function(){
            $rootScope.ErrorActions[mcConst._CMD_.Errors.WrongPassword]();
        }]);
    };

    $rootScope.ErrorActions[248] = function (params) {
        $rootScope.$broadcast('StopPingTimer' ,[]);

        // if (params && (params[0] === 0 || params[0] === "0")){
        //     mcService.SaveSessionSettings("", "", "", false, "");
            // location.reload();
        // } else {
            $rootScope.$broadcast('ErrorMsg', [248, mcConst.ErrorText[248], params, function(){
                $rootScope.ErrorActions[mcConst._CMD_.Errors.WrongPassword]();
            }]);
        // }
    };

    $rootScope.ErrorActions[mcConst._CMD_.Errors.UserNotExist] = function () {
        // var _name  = webix.storage.local.get(mcConst.storageFields.Login);
        // var _email = webix.storage.local.get(mcConst.storageFields.Email);
        //
        // if (_name && _email){
        //     $scope.loginField.setValue(_name);
        //     $scope.emailField.setValue(_email);
        //
        //     $scope.loginBtnClick();
        // } else {
            $rootScope.ErrorActions[mcConst._CMD_.Errors.WrongPassword]();
        // }
    };

    // ===============================

    function hideLoginForm() {
        $scope.doLogin = true;

        _view = $$($scope.Name);

        if (_view && _view.isVisible && _view.isVisible()){
            _view.hide();
        }

        $rootScope.loginFormNotShowed = true;

        mcService.hideFrame("languageChanger");
    }

    function getSearchQuery(src, condition, simple) {
        var data = null;

        if (condition === "ref") {
            data = src.split('&ref=')[1];
        } else {
            src = mcService.copy(src, 1).split('&');

            for (var i = 0; i < src.length; i++){
                if ((condition && mcService.isFunction(condition) && condition(src[i])) || !condition || src[i].indexOf(condition + "=") === 0){
                    if (simple) {
                        data = decodeURI(src[i].split('=')[1]);

                        break;
                    } else {
                        if (!data){
                            data = [];
                        }

                        data.push(src[i]);
                    }
                }
            }
        }

        return data;
    }

    function nextStepShow(args) {
        var lang    = args ? args[0] : mcConst.Lang;
        var onlyChangeLang = $scope.lang != undefined && $scope.lang !== lang;
        var _login  = "";
        var _pwd    = "";

        if (_view && _view.show && !onlyChangeLang){
            $scope.doLogin = false;

            _view.show();

            $rootScope.loginFormNotShowed = false;
        } else

        if (!_view || (_view && _view.isVisible && !_view.isVisible()) || (args[0] && $scope.lang != args[0])){
            $scope.doLogin = false;

            $scope.isWebClient = $rootScope.isWebClient;

            if (_view){
                _login  = $scope.loginField   .getValue() || mcConst.LoginInfo.login;
                _pwd    = $scope.passwordField.getValue();
            }

            mcConst.Lang = lang;

            $scope.show.apply(null, args);

            if ($scope.loginField){
                $scope.loginField   .setValue($scope.login || _login);
                $scope.passwordField.setValue(_pwd   );
            }
        }
    }

    function showLoginForm(args) {
        var group  = getSearchQuery(window.location.search, "group",  true);
        var select = getSearchQuery(window.location.search, "select", true);
        var uins   = getSearchQuery(window.location.search, "uin",    true);
        var invite = getSearchQuery(window.location.search, "invite", true);
        var custom = getSearchQuery(window.location.search, "custom", true);
        var refLink = getSearchQuery(window.location.search, "ref", true);

        if (uins === "0"){
            // uins = webix.storage.local.get(mcConst.storageFields.chatWith);
            uins = mcService.storage.get(mcConst.storageFields.chatWith) || 0;
        }

        if (refLink){
            $rootScope.reflink = refLink;
        }

        $rootScope.initialParams = {
            group : group,
            uins  : uins,
            select: !!select
        };

        $rootScope.customField = custom ? " custom:" + custom : "";

        if (invite){
            nextStepShow(args);
        } else {
            $rootScope.SendCMDToServer([
                group ? mcConst._CMD_.ns_get_by_groups : mcConst._CMD_.ns_get_by_uins,
                group || uins,

                function (data) {
                    $scope.operList     =  mcService.StringToObj(data);
                    $scope.enableSelect = !!select;

                    // $rootScope.chatWith = webix.storage.local.get(mcConst.storageFields.chatWith);
                    $rootScope.chatWith = mcService.storage.get(mcConst.storageFields.chatWith);

                    if (!hasUserInOperList($rootScope.chatWith)){
                        $rootScope.chatWith = null;
                        
                        // webix.storage.local.remove(mcConst.storageFields.chatWith);
                        mcService.storage.remove(mcConst.storageFields.chatWith);
                        // webix.storage.local.remove(mcConst.storageFields.Login);
                    }

                    nextStepShow(args);
                }
            ]);
        }
    }

    function getUser(){
        var online = [];

        $scope.operList.forEach(function (user) {
            if (user.State >= 0){
                online.push(user.UIN);
            }
        });

        if (!online.length){
            $scope.operList.forEach(function (user) {
                online.push(user.UIN);
            });
        }

        return online[mcService.getRandomInt(0, online.length - 1)];
    }

    function getOnlineOperators(){
        var online = [];

        $scope.operList.forEach(function (user) {
            if (user.State >= 0){
                online.push(user);
            }
        });

        return online;
    }

    function hasUserInOperList(uin){
        return mcService.findItemInArrayOfObj($scope.operList, uin, "UIN") !== -1;
    }

    // ===============================

    $scope.keyLogin = function(code , e){
        if (code === 13 && !e.ctrlKey && !e.shiftKey && !e.altKey) {
            $scope.loginBtnClick();

            return false;
        }
    };

    $scope.checkLangPass = function(displayLang, field){
        var lng      = displayLang;
        var lastChar = field.getValue()[field.getValue().length - 1];

        if (lastChar){
            if (lastChar.match(/[a-zA-Z]/)) {
                lng.define('template', 'EN');
                lng.refresh();
            } else
            if (lastChar.match(/[а-яА-Я]/)){
                lng.define('template', 'RU');
                lng.refresh();
            } else
            if (lastChar.match(/[їЇіІєЄ]/)){
                lng.define('template', 'UA');
                lng.refresh();
            }
        }
    };

    $scope.loginBtnClick = function (){
        var isValid = $scope.loginForm.validate() || ($scope.fields === null);

        if (isValid && $scope.loginBtn.isEnabled()) {
            $scope.loginBtn.disable();

            var name = $scope.loginField.getValue() || ("user_" + mcService.formatDate(new Date(), "yyyymmddhhnnsszzz"));
           
            $rootScope.$broadcast('SendCMDToServer' , [
                mcConst.lockInterface,
                mcConst._CMD_.Register,
                
                name.slice(0,64),
                mcService.RandomHash(10),
                "",
                $scope.remember,
                $scope.emailField.getValue(),
                0
            ]);

            mcConst.LoginInfo.login = name; //$scope.loginField.getValue();
            mcConst.LoginInfo.rm    = true;
        } else {
            webix.message({ type:"error", text: mcService.Lang(7) , expire : mcConst.errMessageExpire}); // "7" : "Ошибка входа: неправильно введены данные"
        }
    };

    $scope.changeLang = function(lang){
        $rootScope.$broadcast('ChangLanguage', mcService.detectLang(lang));
    };

    $scope.selectUser = function(user){
        $rootScope.chatWith = this.getList().getItem(user).UIN;
    };

    $scope.show = function(lang){
        var onlyChangeLang = $scope.lang != undefined && $scope.lang !== lang;
        var invite   = getSearchQuery(window.location.search, "invite", true);
        var groupInvite = getSearchQuery(window.location.search, "groupInvite", true);
        var _name    = getSearchQuery(window.location.search, "name",  true);
        var _email   = getSearchQuery(window.location.search, "email", true);
        var hasUser  = getSearchQuery(window.location.search, "uin", true);
        var hasGroup = getSearchQuery(window.location.search, "group", true);

        $rootScope.inFrame = !!getSearchQuery(window.location.search, "inFrame", true); 

        $scope.oldLink      = window.location.search.indexOf('silent') !== -1;
        $scope.fields       = getSearchQuery(window.location.search, "fields", true);
        $rootScope.chatWith = hasUser || $rootScope.chatWith; //$location.param('uin');

        var search  = getSearchQuery(window.location.search, function (item) {
            return item.indexOf('invite') === -1;
        });

        window.history.pushState(null, document.title, window.location.pathname + (search && search.length ? "?" + search.join('&') : "") + window.location.hash);

        $scope.lang  = lang || mcConst.Lang;
        $scope.login = mcConst.LoginInfo.login || "";

        $scope.remember = mcConst.LoginInfo.rm;

        // webix.storage.local.put('cliLang', $scope.lang);
        mcService.storage.put('cliLang', $scope.lang);

        if (invite) {
            $rootScope.SendCMDToServer([
                mcConst.lockInterface,
                mcConst._CMD_.ns_check_invite,
                invite,
                
                function (data) {
                    var user = {};

                    try {
                        user = JSON.parse(data);
                    } catch (e){}

                    if (user.passed) {
                        mcConst.UserInfo.hasInvite = true;

                        if (user.uid) {
                            $rootScope.chatUID = user.uid;
                        } else {
                            $rootScope.chatWith = user.uinwith;
                        }

                        $rootScope.$broadcast('StopPingTimer', []);

                        mcConst.clientType = mcConst._CMD_.ClientType.GUEST;
                        
                        $rootScope.SendCMDToServer([
                            mcConst.lockInterface,
                            mcConst._CMD_.Login,
                            user.uin,
                            user.pass,
                            "",
                            true
                        ]);
                    } else {
                        $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock
                        
                        webix.ui({
                            view    : "window",
                            position: "center",
                            modal   : true,
                            width   : 350,
                            height  : 70,
                            head    : mcLang(503), //"503":"Ваше приглашение недействительно",
                            body    : { rows:[] }
                        }).show();
                    }
                }
            ]);
            
        } else
        
        if (!onlyChangeLang && $scope.remember && mcConst.LoginInfo.login) {
            $rootScope.loginFormNotShowed = !_view || (_view && _view.isVisible && !_view.isVisible());

            if (!$rootScope.chatWith && !hasGroup) {
                // $rootScope.chatWith = webix.storage.local.get(mcConst.storageFields.chatWith);
                $rootScope.chatWith = mcService.storage.get(mcConst.storageFields.chatWith);
            }

            if (!hasUserInOperList($rootScope.chatWith)){
                $rootScope.chatWith = null;
            }

            if (!$rootScope.chatWith){
                $rootScope.chatWith = getUser();
            }

            if (!$rootScope.chatWith){
                $rootScope.$broadcast('ErrorMsg', [-1, mcLang(731), '', function(){ // "731":"В указанной группе операторов нет людей, нельзя подключиться!",
                    location.search = "";
                    location.reload();
                }]);
            } else {
                $rootScope.SendCMDToServer([
                    mcConst.lockInterface,
                    mcConst._CMD_.Login,
                    mcConst.LoginInfo.login,
                    mcConst.LoginInfo.pwd,
                    mcConst.LoginInfo.servPwd,
                    mcConst.LoginInfo.rm
                ]);
            }
        } else {
            webix.ready(function () {
                _view = initLogin($scope);

                if (!hasUser){
                    if ($scope.operList && $scope.operList.length > 1){
                        if (!$scope.enableSelect){
                            $rootScope.chatWith = getUser();
                        } else {
                            $scope.selectOperator.show();

                            var list = $scope.selectOperator.getList();

                            list.define("template", function (obj) {
                                return mcService.myReplaceFormated("<span class='#{color}'>#{name}#{state}</span>", { // #{job}
                                    name  : obj.DisplayName,
                                    color : obj.State === -1 ? "Gray" : "",
                                    // job   : obj.JobPosition ? ", " + obj.JobPosition : "",
                                    state : obj.State === -1 ? " (" + mcLang(29) + ")" : "" // "29" :"Не в сети",
                                });
                            });
                            list.refresh();
                            list.parse(getOnlineOperators().length ? getOnlineOperators() : $scope.operList);
                            list.sort(function (a,b) {
                                return mcService.sortAB(a.DisplayName, b.DisplayName, true);
                            });
                            list.sort(function (a,b) {
                                return mcService.sortAB(a.State, b.State, true);
                            });

                            $scope.selectOperator.setValue(list.getFirstId());
                        }
                    } else

                    if ($scope.operList && $scope.operList.length === 1){
                        $scope.selectOperator.hide();

                        $rootScope.chatWith = $scope.operList[0].UIN;
                    }
                }

                console.log("chatWith: " + $rootScope.chatWith);

                if (!$rootScope.chatWith){
                    $rootScope.$broadcast('ErrorMsg', [-1, mcLang(731), '', function(){ // "731":"В указанной группе операторов нет людей, нельзя подключиться!",
                        location.search = "";
                        location.reload();
                    }]);
                } else {
                    if (_name && _email){
                        $scope.loginField.setValue(_name);
                        $scope.emailField.setValue(_email);

                        $scope.loginBtnClick();
                    } else

                    if ((!$scope.enableSelect && ($scope.fields === null) && !$scope.oldLink) || (!_name && !_email && $rootScope.chatWith && $scope.operList.length === 1 && !hasUser && !$scope.fields)){
                        $scope.loginBtnClick();
                    } else {
                        _view.show();
                    }

                    $rootScope.loginFormNotShowed = false;
                }
            });
        }
    };

    // =============================================

    $scope.$on('enable' + $scope.Name, function enableForm (){
        if (_view) _view.enable();
    });

    // ====================================

    $scope.$on('hide' + $scope.Name, function () {
        hideLoginForm();
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        showLoginForm(args);
    });

    $rootScope.$broadcast('windowResize', [function(){
        if (isMobile && _view){
            _view.resize();
        }
    }]);
}

function initChatFrame($scope){
    "use strict";

    var chatFrame = {
        id: $scope.Name,
        cols: [
            { id: "leftSpace" },
            { id: "centerSpace", maxWidth: 1000, rows: [
                // -- Header --
                {
                    css: "borderBottom whiteBG", cols:
                        [
                            // { id: "hideMainMenu", view: "toggle", width: 35, offIcon: "chevron-left", onIcon: "chevron-right", type:"icon", css: "iconNoPadding noBG relative",
                            //     tooltip: mcLang(699) + "/" + mcLang(698).toLowerCase() + " " + mcLang(706).toLowerCase(), // "698":"Скрыть", "699":"Показать", "706":"Меню",
                            //     offLabel: "&nbsp;", onLabel: "&nbsp;",
                            // on: { onChange: $scope.showHideMainMenu }
                            // },
                            // { id: "miniUserPhoto", view: "template", template: $scope.noImage, hidden: true, borderless: true, width: 40, css: "byCenter" },

                            {
                                view: "button", /*type: "icon", icon: "chevron-down",*/ id: "displayName" + $scope.Name, label: "\u2022\u2022\u2022", css: "imgBtn frameTopBtnLeft color54",
                                click: $scope.titleMenu
                            }
                        ].concat($scope.callButtons).concat({
                            view: "button", id: "btnTopContextMenu", type: "icon", icon: "ellipsis-v", width: 33, css: "icoButton byCenter button_primary iconNoPadding", click: function () {
                                var btn = this.getNode();

                                setTimeout(function () {
                                    $scope.topContextMenu.show(btn, { pos: 'bottom', x: -7, y: 10 });
                                }, 100);
                            }
                        })
                },

                // -- Search --
                {
                    id: "searchWrapper" + $scope.Name, hidden: true, padding: 0, css: "noBorderAll borderTopBottom", height: 30, cols: [
                        { id: "searchChatText" + $scope.Name, view: "text", css: "noBGColorAll", borderless: true, placeholder: mcService.Lang(579) }, // "579":"Найти текст в чате...",

                        {
                            id: "btnCloseSearchText" + $scope.Name, view: "button", type: "icon", icon: "close", width: 35, css: "noBG iconNoPadding byCenter",
                            click: $scope.hideFindChatText
                        }
                    ]
                },

                { height: 10 },

                // -- DownloadUpload --
                $scope.duViews.progressWrapper,
                $scope.dirDuViews.progressWrapper,
                // $scope.duViews.downloadFilesQuestion,

                // -- MediaCall --
                {
                    id: "videoFrameWrapper", hidden: true, css: "relative noBGColor byCenter", borderless: true, gravity: 2, minHeight: 90,
                    template: '<div id="' + mcConst.videoContainer + '" style="height: 100%;"></div>'
                },

                { view: "resizer", id: "videoResizer", hidden: true },

                // -- Main Chat --
                {
                    rows: [
                        {
                            id: "chatTextOutput" + $scope.Name, view: "template", minHeight: 100, borderless: true, css: "noBG selectable", // scrollMenu
                            scroll: false,
                            onClick: $scope.inChatClick,
                            template: ""
                            //'<div id="loadMoreHistory" class="right loadHistory hidden"><a href="#" class="selected">' + mcLang(577) + '</a></div>' + // "577":"Загузить еще сообщения",
                            // '<div id="UIN-0" class="paddingLeft7 scrollMenu hiddenScrollMenu"></div>' // framePaddingР30
                        },

                        {
                            height: 20, cols: [
                                { id: 'framePrivateTypingTextNotify', template: ' ', borderless: true, css: "noBGColor font90 paddingLeft7 lHeight20" },
                                {},
                                { id: 'frameLoadHistory', template: ' ', borderless: true, css: "noBGColor font90 paddingLeft7 byRight lHeight20", autowidth: true },
                                { id: 'scrollDownButton', view: "button", width: 26, type: "icon", icon: "caret-down", css: "iconNoPadding", click: $scope.scrollDown, tooltip: mcLang(711) }, // "711":"Прокрутить чат вниз",
                                { width: 15 }
                            ]
                        }
                        // { height: 24}
                    ]
                },

                { view: "resizer", id: "inputResizer" + $scope.Name, css: "noBGColorAll noBorder" },

                // -- Input Text --
                {
                    id: "inputWrapper", height: mcConst.minEnterTextHeight, minHeight: mcConst.minEnterTextHeight, css: "whiteBG", cols: [
                        {
                            width: 56, rows: [
                                {
                                    id: "smileysButton", view: "button", type: "icon", icon: "smile-o", css: "ico30 borderTop",
                                    on: {
                                        onItemClick: function () {
                                            var btn = this.getNode();

                                            setTimeout(function () {
                                                $scope.smilesWindow.show(btn, { pos: 'top', x: 0, y: -10 });
                                            }, 100);
                                        }
                                    }
                                },
                                {
                                    id: "clipPopupButton", view: "button", type: "icon", icon: "paperclip", css: "ico30", hidden: false,
                                    on: {
                                        onItemClick: function () {
                                            var btn = this.getNode();

                                            setTimeout(function () {
                                                $scope.clipPopupWindow.show(btn, { pos: 'top', x: 0, y: -10 });
                                            }, 100);
                                        }
                                    }
                                }
                            ]
                        },

                        {
                            css: "borderTop", rows: [
                                {
                                    id: "replyBlockWrapper", css: "borderBottom lHeight28 _info", hidden: true, height: 30, cols: [
                                        { id: "replyBlock", template: "", borderless: true, css: "replyBlock" },
                                        { view: "button", value: "x", width: 32, css: "icoButton byCenter button_primary iconNoPadding", click: $scope.closeReplyMsg },
                                        { width: 4 }
                                    ]
                                },

                                {
                                    id: "enterChatText" + $scope.Name, view: "textarea", css: "noBGColorAll", minHeight: 32, keyPressTimeout: 50, borderless: true, placeholder: mcService.Lang(27), // "27" : "Введите ваше сообщение здесь",
                                    on: {
                                        onKeyPress: $scope.typingNotify
                                    }
                                },
                            ]
                        },

                        {
                            id: "btnSendText", view: "button", type: "icon", icon: "send", width: 35, css: "noBG noBorderAll iconNoPadding byCenter borderTop", click: function () {
                                $scope.sendText();

                                setTimeout(function () {
                                    $scope.enterChatText.focus();
                                }, 50);
                            }
                        }
                    ], on: {
                        onViewResize: $scope.saveTextInputSize
                    }
                }
            ]
        },
        { id: "rightSpace" }
    ]};

    // ===========================================================

    $scope.clipPopupWindow   = webix.ui({
        view : "context",
        id   : "clipPopup",
        head : false,
        css  : "noUpperCase",
        padding: 0,

        body : { width: 230, rows: [
            // { id: "sendTempMsgBtn" + $scope.Name, view: "button", type: "icon", icon: "comment-o", css: "icoButton", height: 27,
            //     click: function () {
            //         $scope.visualMsgTempList.show(this.getNode(), {pos: "right", y: -5});
            //     }, label: mcService.Lang(654) // "654":"Вставить фразу",
            // },
                
            { id: "sendImageBtn" + $scope.Name, view: "button", type: "icon", icon: "file-image-o", css: "icoButton", height: 27,
                label: mcService.Lang(53), click: $scope.popupInsertImage }, // "53" : "изображение",

            { id: "sendFilesBtn" + $scope.Name, view: "button", type: "icon", icon: "files-o", css: "icoButton", height: 27,
                label: mcService.Lang(568), click: $scope.popupInsertFile },  // "568":"Файл",

            // { id: "sendVoiceMsgBtn" + $scope.Name, view: "button", type: "icon", icon: "microphone", css : "icoButton", height: 27,
            //     label: mcService.Lang(671), click: $scope.startVoiceMessage } // "671":"Голосовое сообщение",

        ]}
    });

    $scope.smilesWindow      = webix.ui({
        view : "context",
        id   : "smilesWindow",
        head : false,
        css  : "noUpperCase",
        padding: 0,

        body : { id: "smilesWindowTemp", width: 380, template: $scope.drawSmiles, height: 180, /*autoheight: true,*/ borderless: true, onClick: $scope.sendSmile}
    });

    $scope.topContextMenu    = webix.ui({
        view : "context",
        id   : "topContextMenu",
        head : false,
        css  : "noUpperCase",
        padding: 0,

        body : { width: 300, rows: [
            { id: "clearMsgHistoryBtn" + $scope.Name, view: "button", type: "icon", icon: "calendar-o", css: "icoButton", height: 27,
                click: $scope.clearHistoryContext, label: mcService.Lang(683)}, // "683":"Скрыть загруженную историю",

            // { id: "quitFromConf" + $scope.Name, view: "button", type: "icon", icon: "sign-out", css: "icoButton", height: 27,
            //     click: $scope.quitFromConfContext, label: mcService.Lang(28)}, // "28" :"Выйти из конференции",

            // { id: "closePrivate" + $scope.Name, view: "button", type: "icon", icon: "sign-out", css: "icoButton", height: 27,
            //     click: $scope.closePrivateContext, label: mcService.Lang(552)} // "552":"Закрыть диалог",
        ]}
    });

    // $scope.userContextMenu   = webix.ui({
    //     view : "context",
    //     id   : "userContextMenu",
    //     head : false,
    //     css  : "noUpperCase",
    //     padding: 0,
    //
    //     body : { width: 230, rows: [
    //         { id: "openPrivate" + $scope.Name, view: "button", type: "icon", icon: "comments-o", css: "icoButton", height: 27,
    //             click: $scope.openDialogFromContext, label: mcService.Lang(685)}, // "685":"Открыть диалог",
    //
    //         { id: "getUserInfo" + $scope.Name, view: "button", type: "icon", icon: "info-circle", css: "icoButton", height: 27,
    //             /*click: $scope.getUserInfoFromContext,*/ label: mcService.Lang(721)}, // "721":"Профиль пользователя",
    //     ]},
    //
    //     on: {
    //         onShow: $scope.showContextUserMenu,
    //         onHide: $scope.hideContextUserMenu
    //     }
    // });

    // $scope.visualMsgTempList = webix.ui({
    //     view     :"submenu",
    //     id       :"quickMsgTempSubMenu",
    //     padding  : 0,
    //     autowidth: true,
    //     template : function(obj){
    //         return "<div class='quickMsgTempMsg' title='" + (obj.id === 9999 ? "" : obj.value) + "'>" + (obj.value ? obj.value : "- - - - -") + "</div>";
    //     },
    //     type     : { height: 26 },
    //     on       : { onItemClick: $scope.sendQuickMsg }
    // });

    // ===========================================================

    var view = webix.ui(chatFrame);

    // $scope.callManager.initCallBtns();

    $scope.centerSpace        = $$("centerSpace");
    $scope.leftSpace          = $$("leftSpace");
    $scope.rightSpace         = $$("rightSpace");

    // -- Header --
    $scope.displayName       = $$("displayName"    + $scope.Name);
    $scope.PrivateTypingTextNotify = $$("framePrivateTypingTextNotify");
    $scope.frameLoadHistory  = $$("frameLoadHistory");
    // $scope.hideMainMenu      = $$("hideMainMenu");

    // $scope.HideMainMenu.define("onLabel", "<span id='_uniBadge' class='badge_tool toRight hidden'> </span>");
    // $scope.HideMainMenu.refresh();

    // $scope.hideMainMenu.getNode().appendChild($scope.getUniBadge());

    // -- Top Context Menu --
    $scope.clearMsgHistoryBtn= $$("clearMsgHistoryBtn" + $scope.Name);
    $scope.quitFromConf      = $$("quitFromConf" + $scope.Name);
    $scope.closePrivate      = $$("closePrivate" + $scope.Name);
    // -- Main Chat --
    $scope.chatTextOutputW   = $$("chatTextOutput" + $scope.Name);
    $scope.chatTextOutput    = $$("chatTextOutput" + $scope.Name).$view.lastChild;
    // -- Input Text --
    $scope.inputResizer      = $$("inputResizer"   + $scope.Name);
    $scope.inputWrapper      = $$("inputWrapper");
    $scope.enterChatText     = $$("enterChatText"  + $scope.Name);
    // $scope.enterChatTextT    = $$("enterChatText"  + $scope.Name).getInputNode();
    $scope.btnSendText       = $$("btnSendText");
    $scope.clipPopupButton   = $$("clipPopupButton");
    $scope.smileysButton     = $$("smileysButton");
    $scope.replyBlock        = $$("replyBlock");
    $scope.replyBlockWrapper = $$("replyBlockWrapper");
    // -- Search --
    $scope.searchWrapper     = $$("searchWrapper"  + $scope.Name);
    $scope.searchChatText    = $$("searchChatText" + $scope.Name);
    // -- MediaCall --
    $scope.videoResizer      = $$('videoResizer');
    $scope.videoFrameWrapper = $$('videoFrameWrapper');

    $scope.smilesWindowTemp  = $$("smilesWindowTemp");

    // $scope.miniUserPhoto     = $$("miniUserPhoto");

    $scope.sendFilesBtn      = $$("sendFilesBtn"   + $scope.Name);
    $scope.sendImageBtn      = $$("sendImageBtn"   + $scope.Name);
    // $scope.sendTempMsgBtn    = $$("sendTempMsgBtn" + $scope.Name);
    $scope.sendVoiceMsgBtn   = $$("sendVoiceMsgBtn"+ $scope.Name);

    // $scope.chatTextOutputW.getNode().onmousedown = $scope.scrollControl;

    // $scope.loadMoreHistory          = document.getElementById("loadMoreHistory");
    // $scope.loadMoreHistory.onclick  = $scope.addHistoryMessages;

    // $scope.chatTextOutput.style.position = 'relative';

    // $scope.chatTextOutputW.getNode().addEventListener("wheel", $scope.onWheel);

    // -- DownloadUpload --
    // $scope.initDU();

    return view;
}
function chatFrameController($scope, $rootScope, mcPlaySound, drawMessage, /*mcCallManager,*/ mcVoiceRecorder, mcEmojiList){
    "use strict";

    $scope.Name = mcConst.dataModels.ChatFrame;
    
    $scope.displayName        = null;
    $scope.chatTextOutput     = null;
    $scope.enterChatText      = null;
    $scope.clipPopupButton    = null;
    $scope.clipPopupLoaderImg = null;
    $scope.videoFrameWrapper  = null;
    // $scope.callManager        = mcCallManager;
    $scope._rights            = {
        QBroadcasts: false,
        QFilesSend: false,
        QAllowFindUsersTool: false,
        QSendImages: false,
        QImagesPrivates: false,
        QTxtConfSendFiles: false,
        QPrivInsertFiles: false,
        QCommonAllowQuickMessages: false,
        QAllowRedirectDialogs: false,
        QAllowViewRemoteUserProfile: false
    };
    $scope.noImage            = '<img class="borderBottom" id="upFotoCanvas" src="' + mcConst.imagesPath.all + 'noimage.png" border="0" style="max-height: 99%">';

    // ----------------------------------

    var pressNotifyCount  = 0;
    var prevFrame         = $rootScope.chatAliases.Elisa;
    var currentFrame      = null;
    var frameID           = "";
    var mainView          = null;
    var titleValue        = [];
    var prevHeight        = 0;
    var msgTemplateList   = [];
    var currentSmilesSet  = "1";
    var mstTemplate       = "";
    var cursorPosition    = 0;
    var timerTypingNotify = null;
    var noCreate          = true;
    var contextUser       = null;
    var _uniBadgeCount    = 0;
    var menuIsHidden      = false;
    var deferredLoadImages= null;
    var saveMsgTempList   = {};
    var sendMsgBackup     = {};
    var msgBackupCount    = 5;
    var msgBackupIdx      = msgBackupCount;
    var replyToMsgIdx     = 0;

    var old_shit_type = {
        smile: 1
    };

    // ----------------------------------

    var framesList            = new McFramesList(_onWheel);
    var downloadUploadManager = new DownloadUploadManager($rootScope, {
        name: $scope.Name,
        images: {
            enable: true
        },
        upload  : {
            enable: true,
            done  : _sendText
        },
        download: {
            enable: true,
            ftp_download: true
        }
    });
    var directDownloadUpload  = new DownloadUploadManager($rootScope, {
        name    : $scope.Name + "Direct",
        direct  : {
            enable: true
        }
    });

    drawMessage.framesList = framesList;
    drawMessage.scrollDown = scrollDown;

    // ----------------------------------
    
    var userMenu = function() {
        return [{
            view: "button",
            value: mcService.Lang(41), // "41" : "Добавить в игнор",
            click: function(){
                this.getTopParentView().hide();

                webix.confirm({
                    title : mcService.Lang(42), // "42" : "Игнорировать приватные сообщения от:",
                    ok    : mcService.Lang(43), // "43" : "Да",
                    cancel: mcService.Lang(44), // "44" : "Нет",
                    type  :"confirm-error",
                    text  : titleValue[0],
                    width : 310,
                    callback:function(result){ //setting callback
                        if (result){
                            var uin = $rootScope.GetChatID();

                            $rootScope.$broadcast('SendCMDToServer', [
                                mcConst._CMD_.cs_set_custom_ignore,
                                mcConst.SessionID,
                                uin, // current UIN
                                mcConst.ignores.private
                            ]);

                            $rootScope.$broadcast('getIgnoreList', []);
                            $rootScope.$broadcast('addIgnoreInPrivateInfo', [uin]);
                        }
                    }
                });
            }
        }, {
            view: "button",
            value: mcService.Lang(51), // "51" : "Удалить из игноров",
            click: function(){
                var uin = $rootScope.GetChatID();

                this.getTopParentView().hide();

                $rootScope.$broadcast('SendCMDToServer', [
                    mcConst._CMD_.cs_del_custom_ignore,
                    mcConst.SessionID,
                    uin,
                    mcConst.ignores.private
                ]);

                $rootScope.$broadcast('getIgnoreList', []);
                $rootScope.$broadcast('removeIgnoreInPrivateInfo', [uin]);

                webix.message(mcService.Lang(52, titleValue[0])); //"52" : "С пользователя %s снят игнор!",
            }
        }];
    };
    var confMenu = function() {
        return [{
            view : "button",
            value: mcService.Lang(28), // "28" : "Выйти из конференции",
            click: function(){
                this.getTopParentView().hide();

                $rootScope.$broadcast('leaveConf', [getCurrentID()]);
            }
        }];
    };

    // =====================================

    // window._userActions.userMenu      = function (ev){
    //     var uin = this.getAttribute('uin');
    //     var btn = this;
    //
    //     if (!mcService.IM(uin) && $rootScope.GetChatTypeID() !== "UIN-0"){
    //         contextUser = uin;
    //
    //         if ($rootScope.GetChatType() !== "UID" && $rootScope.GetChatTypeID() === frameID) {
    //             $$("openPrivate" + $scope.Name).hide();
    //         } else {
    //             $$("openPrivate" + $scope.Name).show();
    //         }
    //
    //         $scope.userContextMenu.show(btn, {pos: 'bottom', x: -10, y: 10});
    //     } else {
    //         contextUser = null;
    //
    //         $scope.userContextMenu.hide();
    //     }
    // };

    window._userActions.hideStatus    = function (){
        mcService.hideView(this.querySelector(".statusItem"));
    };

    window._userActions.showStatus    = function (){
        mcService.showView(this.querySelector(".statusItem"));
    };

    // =====================================

    function getCurrentID() {
        return frameID.split('-')[1];
    }

    function getCurrentType() {
        return frameID.split('-')[0];
    }

    // =============================================

    function resizeMainWindow(w, h) {
        if ($scope.centerSpace){
            if (w <= 800) {
                $scope.leftSpace.hide();
                $scope.rightSpace.hide();
                $scope.centerSpace.config.minWidth = null;
            } else {
                $scope.leftSpace.show();
                $scope.rightSpace.show();
                $scope.centerSpace.config.minWidth = 800;
            }

            mainView.adjust();
        }
    }

    function _deleteMessage(yes, uinUID, idx, cmd, trash, parent) {
        if (yes){
            trash.parentElement.className += " deletedMsg";

            parent.removeChild(trash);

            $rootScope.SendCMDToServer([
                cmd,
                mcConst.SessionID,

                uinUID,
                idx,
                0     // удаление для всех
            ]);
        } else {
            focusToEnterChat();
        }
    }

    function transformRusText(text) {
        var listSymbols = {
            en : "qwertyuiop[]asdfghjkl;'zxcvbnm,.QWERTYUIOP{}ASDFGHJKL:ZXCVBNM<>`~\"",
            ru : "йцукенгшщзхъфывапролджэячсмитьбюЙЦУКЕНГШЩЗХЪФЫВАПРОЛДЖЯЧСМИТЬБЮёЁЭ",
            uk : "йцукенгшщзхїфівапролджєячсмитьбюЙЦУКЕНГШЩЗХЇФІВАПРОЛДЖЯЧСМИТЬБЮ`~Є",
            ua : "йцукенгшщзхїфівапролджєячсмитьбюЙЦУКЕНГШЩЗХЇФІВАПРОЛДЖЯЧСМИТЬБЮ`~Є"
        };
        var res = new Array(text.length);
        var idx = -1;

        for (var i = 0; i < text.length; i ++){
            idx = listSymbols.en.indexOf(text[i]);
            
            if (idx >= 0) {
                res[i] = listSymbols[mcConst.Lang][idx];
            } else {
                idx = listSymbols.ru.indexOf(text[i]);
                
                if (idx >= 0){
                    res[i] = listSymbols.en[idx];
                } else {
                    res[i] = text[i];
                }
            }
        }

        return res.join('');
    }

    function sendPrivate(UIN, text, msgType){
        var msg = mcService.trim(text || $scope.enterChatText.getValue());

        if (msg !== ''){
            if ($rootScope.isWebClient || mcConst.ClientSettings.SoundsSndChatRet) {
                mcPlaySound.PlaySound(mcPlaySound.Sounds.EnterBtn);
            }

            if (replyToMsgIdx && !text){ // !text - угнор файлов и картинок в роли реплая
                var replMsg = mcService.StringToObj($rootScope.$broadcast(window._messages_.dialogsList.getPrivatHistoryMsg, [UIN || $rootScope.GetChatID(), replyToMsgIdx]));

                if (replMsg.MsgType === mcConst._CMD_.msgType.REPLY){
                    replMsg = {
                        "UIN": replMsg.UINFrom,
                        "Msg": replMsg.Reply.Msg,
                        "Idx": replMsg.Idx,
                        // "MsgType": mcConst._CMD_.msgType.REPLY,
                        "MsgType": mcConst._CMD_.msgType.TEXT,
                        "dtUTC": replMsg.dtUTC
                    }
                } else {
                    replMsg = {
                        "UIN": replMsg.UIN,
                        "Msg": replMsg.Msg,
                        "Idx": replMsg.Idx,
                        "MsgType": replMsg.MsgType,
                        "dtUTC": replMsg.dtUTC
                    }
                }

                msg = JSON.stringify({
                    "Original": replMsg,
                    "Reply"   : text ? {Msg: mcService.StringToObj(text), MsgType: msgType} : {
                        "Msg"    : msg,
                        "MsgType": msgType === void(0) ? mcConst._CMD_.msgType.TEXT : msgType
                    }
                });

                msgType = mcConst._CMD_.msgType.REPLY;
            }

            // check is link
            if ((msg.toLowerCase().indexOf("http://") === 0 || msg.toLowerCase().indexOf("https://") === 0 ||  msg.toLowerCase().indexOf("www.") ===0) && mcService.CheckLink(msg)){
                msgType = mcConst._CMD_.msgType.ANY_LINK;
                msg     = JSON.stringify({
                    Link : msg,
                    Desc : msg
                });
            } else {
                msg = msgType === undefined || msgType === mcConst._CMD_.msgType.TEXT ? (msg.replace(/\n/gi, '\r\n')) : msg;
            }

            // saveMsgTemplate("", frameID, true);

            mstTemplate = "";

            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_private_msg,
                mcConst.SessionID,

                JSON.stringify({
                    UIN : UIN || $rootScope.GetChatID(),
                    Msg : msg,
                    MsgType: msgType === undefined || msgType === null ? mcConst._CMD_.msgType.TEXT : msgType
                })
            ]);
        }
    }

    function sendConf(UID, text, msgType){
        var msg = mcService.trim(text || $scope.enterChatText.getValue());

        if (msg !== ''){
            if (replyToMsgIdx && !text){
                msg = JSON.stringify({
                    "Original": $rootScope.$broadcast(window._messages_.dialogsList.getConfHistoryMsg, [UID, replyToMsgIdx]),
                    "Reply"   : text ? {Msg: mcService.StringToObj(text), MsgType: msgType} : {
                        "Msg"    : msg,
                        "MsgType": msgType === void(0) ? mcConst._CMD_.msgType.TEXT : msgType
                    }
                });

                msgType = mcConst._CMD_.msgType.REPLY;
            }

            if ($rootScope.isWebClient || mcConst.ClientSettings.SoundsSndChatRet) {
                mcPlaySound.PlaySound(mcPlaySound.Sounds.EnterBtn);
            }

            if ((msg.toLowerCase().indexOf("http://") === 0 || msg.toLowerCase().indexOf("https://") === 0 ||  msg.toLowerCase().indexOf("www.") ===0) && mcService.CheckLink(msg)){
                msgType = mcConst._CMD_.msgType.ANY_LINK;
                msg     = JSON.stringify({
                    Link : msg,
                    Desc : msg
                });
            } else {
                msg = msgType === undefined || msgType === mcConst._CMD_.msgType.TEXT ? (msg.replace(/\n/gi, '\r\n')) : msg;
            }

            // saveMsgTemplate("", frameID, true);
            
            mstTemplate = "";

            $rootScope.$broadcast('SendCMDToServer' , [
                mcConst._CMD_.cs_put_msg2txt_channel,
                mcConst.SessionID,

                JSON.stringify({
                    UIN : mcConst.UserInfo.UIN,
                    UID : UID || $rootScope.GetChatID(),
                    Msg : msg,
                    MsgType: msgType === undefined || msgType === null ? mcConst._CMD_.msgType.TEXT : msgType
                })
            ]);
        }
    }

    function scrollDown(_frameID, now, smooth, history, scrollHeight){ // frame
        var _frame = framesList.get(_frameID, noCreate);

        scrollHeight = scrollHeight || _frame.scrollHeight;

        if (mainView && _frame){
            if (_frame.style.display === "none" || !mainView.isVisible()){
                if (mcService.isFunction(_frame.scrollOnShow)) {
                    _frame.scrollOnShow = null;
                }

                _frame.scrollOnShow = function () {
                    if (_frame.scrollTop !== _frame.scrollHeight) {
                        scrollDown(_frameID, now, smooth, history, scrollHeight);
                    }

                    _frame.scrollOnShow = null;
                }
            } else

            if (history){
                if (_frame.scrollDownTimer){
                    clearTimeout(_frame.scrollDownTimer);

                    _frame.scrollDownTimer = null;
                }

                if (_frame.scrollHeight !== prevHeight && _frame.scrollTop <= 20) {
                    prevHeight = _frame.scrollTop = _frame.scrollTop + scrollHeight;
                }
            } else

            if (now){
                prevHeight = scrollHeight;

                _frame.scrollTop = prevHeight - _frame.offsetHeight;
            } else

            if (!_frame.scrollDownTimer) {
                _frame.scrollDownTimer = setTimeout(function () {
                    _frame.scrollDownTimer = null;

                    prevHeight = _frame.scrollHeight + scrollHeight;

                    if (smooth){
                        _frame.scroll({top: prevHeight, left: 0, behavior: 'smooth'});
                    } else {
                        _frame.scrollTop = prevHeight;
                    }
                }, 100);
            }
        }
    }

    function attachButtons() {
        if (($scope._rights.QTxtConfSendFiles && $rootScope.GetChatType() === $rootScope.chatAliases.UID) ||
            ($scope._rights.QPrivInsertFiles  && $rootScope.GetChatType() === $rootScope.chatAliases.UIN)){
            $scope.sendFilesBtn.show();
        } else {
            $scope.sendFilesBtn.hide();
        }

        if (($scope._rights.QSendImages     && $rootScope.GetChatType() === $rootScope.chatAliases.UID) ||
            ($scope._rights.QImagesPrivates && $rootScope.GetChatType() === $rootScope.chatAliases.UIN)){
            $scope.sendImageBtn.show();
        } else {
            $scope.sendImageBtn.hide();
        }

        // $scope.sendTempMsgBtn[$scope._rights.QCommonAllowQuickMessages ? 'show' : 'hide']();

        if (($rootScope.GetChatType() === $rootScope.chatAliases.UID && !$scope._rights.QTxtConfSendFiles && !$scope._rights.QCommonAllowQuickMessages && !$scope._rights.QSendImages) ||
            ($rootScope.GetChatType() === $rootScope.chatAliases.UIN && !$scope._rights.QTxtConfSendFiles && !$scope._rights.QImagesPrivates && !$scope._rights.QPrivInsertFiles)) {
            $scope.clipPopupButton.hide();

            // webix.html.addCss( $scope.smileysButton.getNode(), "borderTop");
        } else {
            $scope.clipPopupButton.show();
            
            // webix.html.removeCss( $scope.smileysButton.getNode(), "borderTop");
        }
    }

    function hideLoadHistoryFrame() {
        $scope.frameLoadHistory.define('template', ' ');
        $scope.frameLoadHistory.refresh();
    }
    
    function showLoadHistoryFrame() {
        $scope.frameLoadHistory.define('template', "<i class='fa fa-refresh finger brown noPadding fa-spin' style='width: 12px'></i> " + mcLang(682) + "..."); // "682":"Загрузка истории",
        $scope.frameLoadHistory.refresh();
    }

    function fillFrame(topBtnTitle, topTopic){
        frameID  = $rootScope.GetChatTypeID();

        if (mainView) {
            mainView.show();

            framesList.hide(prevFrame, function () {
                var msg = $scope.enterChatText.getValue();

                // msg && saveMsgTemplate(msg, prevFrame, true);

                $scope.enterChatText.setValue("");
            });

            titleValue   = [topBtnTitle, topTopic];
            prevFrame    = frameID;
            currentFrame = framesList.get(frameID);

            replyToMsgIdx = 0;

            currentFrame.show(/*loadMsgTemplate*/);

            // $rootScope.$broadcast('selectDialog',[frameID]);

            $scope.displayName.show();
            $scope.displayName.define('label', topBtnTitle + (topTopic ? ': ' + topTopic : ''));
            $scope.displayName.refresh();

            // $scope.userContextMenu.hide();
            $scope.replyBlockWrapper.hide();

            msgBackupIdx = msgBackupCount;

            switch ($rootScope.GetChatType()){
                case $rootScope.chatAliases.UIN:
                    drawMessage.setMyMessages(mcService.Marge(drawMessage.getMyMessagesTemp(), $rootScope.$broadcast(window._messages_.dialogsList.getMyMessagesState, [$rootScope.GetChatID()])));

                    $scope.topLabelList = userMenu();  // private

                    $scope.centerSpace.getNode().className = "webix_view noMargin gradient webix_layout_line boxShadow";

                    $scope.inputWrapper .show();
                    $scope.inputResizer .show();
                    // $scope.miniUserPhoto.show();

                    // $scope.quitFromConf.hide();
                    // $scope.closePrivate.show();

                    // $scope.callManager.setUserInfo($rootScope.GetChatID(), topBtnTitle);
                    $rootScope.$broadcast(window._messages_.callManager.setUserInfo, [$rootScope.GetChatID(), topBtnTitle]);

                    if (frameID === $rootScope.chatAliases.Elisa){
                        // $scope.miniUserPhoto.define('template', $scope.noImage);
                        // $scope.miniUserPhoto.refresh();
                    }
                break;

                case $rootScope.chatAliases.UID:
                    $scope.topLabelList = confMenu();  // channel

                    $scope.centerSpace.getNode().className = "webix_view noMargin gradient webix_layout_line boxShadow";

                    $scope.inputWrapper .show();
                    $scope.inputResizer .show();
                    // $scope.miniUserPhoto.hide();

                    // $scope.quitFromConf.show();
                    // $scope.closePrivate.hide();
                break;

                default:
                    mainView.getNode().className = "webix_view noMargin webix_layout_line";
            }

            $scope.isConf = ($rootScope.GetChatType() === $rootScope.chatAliases.UID) || $rootScope.GetChatID() == 0;

            if ($scope.isConf) {
                // $scope.callManager.hideCallBtns();
                $rootScope.$broadcast(window._messages_.callManager.hideCallBtns);
            } else {
                // $scope.callManager.showCallBtns();
                $rootScope.$broadcast(window._messages_.callManager.showCallBtns);
            }

            if (currentFrame.scrollTop === 0) {
                scrollDown(frameID, true);
            }

            hideLoadHistoryFrame();

            attachButtons();
        }
    }

    function focusToEnterChat(_pos){
        _pos = _pos || cursorPosition;

        if (mcConst.LoggedIn && $scope.enterChatText && $scope.enterChatText.getInputNode() && $scope.enterChatText && mcService.inArray($rootScope.GetChatType(), [$rootScope.chatAliases.UID, $rootScope.chatAliases.UIN]) >= 0 ){
            cursorPosition = _pos || mcService.getCaretPosition($scope.enterChatText.getInputNode());
            
            $scope.displayName.focus();

            setTimeout(function () {
                $scope.enterChatText.focus();
                
                mcService.setCaretPosition($scope.enterChatText.getInputNode(), cursorPosition);
            }, 50);
        }
    }

    function _sendText(sendType, sendId, textToSend, msgType, oldShitType){
        var id = sendId || $rootScope.GetChatID();
        var chatType = sendType || $rootScope.GetChatType();

        addMsgToBackup({
            Msg    : mcService.trim(textToSend || $scope.enterChatText.getValue()),
            UINFrom: mcConst.UserInfo.UIN,
            UINTo  : chatType === "UIN" ? id : null,
            MsgType: msgType === void(0) ? mcConst._CMD_.msgType.TEXT : msgType,
            UID    : chatType === "UID" ? id : null,
            Mod    : mcConst._CMD_.msgMods.NONE,
            notSend: true
        });

        switch (chatType){
            case 'UIN': sendPrivate(id, textToSend, msgType); break;
            case 'UID': sendConf(id, textToSend, msgType);    break;
        }

        $scope.replyBlockWrapper.hide();
        replyToMsgIdx = 0;

        if (mcService.inArray(msgType, [void(0), mcConst._CMD_.msgType.OLD_SHIT, mcConst._CMD_.msgType.TEXT]) >= 0 && oldShitType !== old_shit_type.smile){
            setTimeout(function(){
                $scope.enterChatText.setValue('');
                $scope.enterChatText.$setValue('');

                cursorPosition = 0;
            }, 10);
        }

        if ($scope.smilesWindow && $scope.smilesWindow.isVisible()) {
            $scope.smilesWindow.hide();
        }

        return false;
    }

    function minMax (isMax, callInfo) {
        var wrapper = $scope.videoFrameWrapper;
        var wrapNode = wrapper.getNode().firstChild;

        if (isMax){
            wrapNode.className = wrapNode.className.replace(" maxSize", "");
            wrapNode.style.position = "relative";

            if (callInfo.cPanel) {
                callInfo.cPanel.body.style.position = "absolute";
            }
        } else {
            wrapNode.className += " maxSize";
            wrapNode.style.position = "fixed";

            if (callInfo.cPanel) {
                callInfo.cPanel.body.style.position = "fixed";
            }
        }

        if (!$rootScope.isWebClient){
            $rootScope.$broadcast(window._messages_.clientData.toggleKioskMode, [!isMax]);
        }
    }

    function showVideoFrame(callInfo) {
        if (callInfo && (callInfo.useVideo || callInfo.receiveScreen)){
            var wrapper = $scope.videoFrameWrapper;
            var wrapNode = wrapper.getNode().firstChild;

            wrapper.show();

            $scope.videoResizer.show();

            $scope.videoFrameContainer = document.getElementById(mcConst.videoContainer);
            $scope.videoFrameContainer.appendChild(callInfo.video);

            if (callInfo.receiveScreen) {
                wrapNode.style.overflow = "auto";
                wrapNode.style.position = "relative";
            } else {
                wrapNode.style.overflow = "none";
                wrapNode.style.position = "";

                $scope.videoFrameContainer.appendChild(callInfo.mirror);

                callInfo.video.style.maxWidth = "100%";
            }

            mainView.getTopParentView().adjust();
            
            callInfo.cPanel.init(wrapNode);

            wrapNode.onmousemove = function () {
                callInfo.cPanel.onmousemove();
            };

            callInfo.cPanel.closeCall = function () {
                $rootScope.$broadcast(window._messages_.callManager.stopCurrentCall);
            };

            callInfo.cPanel.fitFill = function (isFit, videoContainer) {
                if (!isFit){
                    $scope.videoFrameContainer.style.height = "";
                    videoContainer.className = videoContainer.className.replace(" fit", "");
                } else {
                    $scope.videoFrameContainer.style.height = "100%";
                    videoContainer.className += " fit";
                }
            };

            callInfo.cPanel.minMax = function (isMax) {
                minMax(isMax, callInfo);
            };
            
            callInfo.cPanel.show();
        }
    }

    function hideVideoFrame(callInfo){
        var wrapNode = $scope.videoFrameWrapper.getNode();
        
        wrapNode.onmousemove = null;

        $scope.videoFrameWrapper.hide();
        
        $scope.videoResizer.hide();

        if ($scope.videoFrameContainer){
            $scope.videoFrameContainer.innerHTML = "";
        }

        $scope.videoFrameContainer = null;

        mainView.getTopParentView().adjust();

        if (!$rootScope.isWebClient && callInfo){
            minMax(true, callInfo);
        }
    }

    function markGotNotify(UIN, ID){
        var elem  = null;

        var firstIdx = $rootScope.$broadcast(window._messages_.dialogsList.getFirstLastMsg, [UIN, true]);

        for (var idx = ID; idx >= firstIdx; idx --){
            elem = document.getElementById(idx + "-st-" + mcConst.UserInfo.UIN);

            if (elem){
                if (elem.className.indexOf("messageGot") === 0 || elem.className.indexOf("messageRead") === 0){
                    break;
                } else {
                    elem.className = "messageGot statusItem";
                }
            }
        }
    }

    function markReadNotify(UIN, ID){
        var elem  = null;
        var firstIdx = $rootScope.$broadcast(window._messages_.dialogsList.getFirstLastMsg, [UIN, true]);

        for (var idx = ID; idx >= firstIdx; idx --){
            elem = document.getElementById(idx + "-st-" + mcConst.UserInfo.UIN);

            if (elem){
                if (elem.className.indexOf("messageRead") === 0){
                    break;
                } else {
                    elem.className = "messageRead statusItem";
                }
            }
        }
    }

    function imagePastePreview(image, cb) {
        webix.confirm({
            width : 350,
            height: 350,
            position: "center",
            type  : "confirm-warning",
            text  : mcService.myReplaceFormated("#{title}<br><br><div style='height: 235px; vertical-align: center'><img src='#{img}' style='max-height: 230px; max-width: 330px'></div>",{
                title: mcLang(649), // "649":"Отправка изображения",
                img  : window.URL.createObjectURL(image)
            }),
            ok    : mcService.Lang(19), // "19" :"Отправить",
            cancel: mcService.Lang(33), // "33": "Отмена",

            callback: function(yes){
                yes && cb();

                focusToEnterChat();
            }
        });
    }

    function filePastePreview(files, cb) {
        var list = "";

        for (var i = 0; i < files.length; i++){
            list += mcService.insertIco("fa-file-o") + mcService.extractFileName(files[i]) + "<br>";
        }

        webix.confirm({
            width : 350,
            position: "center",
            type  : "confirm-warning",
            text  : mcLang(710) + "<br><br>" + list, // "710":"Отпрвка файлов",
            ok    : mcService.Lang(19), // "19" :"Отправить",
            cancel: mcService.Lang(33), // "33": "Отмена",

            callback: function(yes){
                yes && cb();

                focusToEnterChat();
            }
        });
    }

    function _onWheel(chatView, e){
        // if ($scope.userContextMenu.isVisible()) {
        //     $scope.userContextMenu.hide();
        // }

        if (!chatView.uploadHistoryTimer){
            chatView.uploadHistoryTimer = setTimeout(function () {
                if (chatView.scrollTop < 40) {
                    var _frameID = chatView.chatType + "-" + chatView.chatID;

                    if ($rootScope.GetChatTypeID() === _frameID) {
                        showLoadHistoryFrame();

                        $rootScope.$broadcast(window._messages_.dialogsList.getHistoryMsgRange, [chatView.chatID, chatView.chatType]);

                        setTimeout(function () {
                            if (chatView.uploadHistoryTimer) chatView.uploadHistoryTimer = null;
                        }, 500);
                    } else {
                        scrollDown(_frameID, true);

                        chatView.uploadHistoryTimer = null;
                    }
                } else {
                    chatView.uploadHistoryTimer = null;
                }
            }, 150);
        }
    }

    function updateFrameConfCount (uid, count) {
        if ($rootScope.createUID(uid) === frameID){
            $scope.displayName.define('label', (count ? "(" + count + ") " : "") + (titleValue[1] ? titleValue[1] : titleValue[0]));
            $scope.displayName.refresh();
        }
    }

    function saveCursorPositionAndMsgTemplate(code, onlyPos, msg, _frameID) {
        if (mcService.inArrayB(code, mcConst.keyCodes.PrintAndEdit()) || code === void(0)) {
            // !onlyPos && saveMsgTemplate(msg, _frameID || $rootScope.GetChatTypeID(), true);

            cursorPosition = mcService.getCaretPosition($scope.enterChatText.getInputNode()) || 0;
        }
    }

    function getUserFromOperList(uin){
        var id = mcService.findItemInArrayOfObj($scope.operList, uin, "UIN");

        return id === -1 ? {} : $scope.operList[id];
    }

    function fillOpersList() {
        var list = $scope.selectNewOperator.getList();

        $scope.selectNewOperator.getPopup().hide();
        $scope.selectNewOperator.hide();

        list.clearAll();
        list.define("template", function (obj) {
            return mcService.myReplaceFormated("<span class='#{color}'>#{name}#{state}</span>", { // #{job}
                name  : obj.DisplayName,
                color : obj.State === -1 ? "Gray" : "",
                // job   : obj.JobPosition ? ", " + obj.JobPosition : "",
                state : obj.State === -1 ? " (" + mcLang(69) + ")" : "" // "69" :"офлайн",
            });
        });
        list.refresh();
        list.parse($scope.operList);
        list.sort(function (a,b) {
            return mcService.sortAB(a.DisplayName, b.DisplayName, true);
        });
        list.sort(function (a,b) {
            return mcService.sortAB(a.State, b.State, true);
        });

        $scope.noChangeUser = true;
        $scope.selectNewOperator.setValue($rootScope.chatWith);
        $scope.noChangeUser = false;

        $scope.selectNewOperator.show();
    }

    function getOperatorsList(cb){
        $rootScope.SendCMDToServer([
            $rootScope.initialParams.group ? mcConst._CMD_.ns_get_by_groups : mcConst._CMD_.ns_get_by_uins,
            $rootScope.initialParams.group || $rootScope.initialParams.uins,

            true, // request in online

            function (data) {
                $scope.operList = mcService.StringToObj(data);

                $scope.operList.forEach(function (user) {
                    user.id = user.UIN;
                });

                cb && cb();
            }
        ]);
    }

    // =====================================

    function ctrlUp() {
        if (sendMsgBackup[frameID]){
            var msg = sendMsgBackup[frameID][msgBackupIdx - 1];

            if (msg && msg.Msg) {
                if (msg.Msg === $scope.enterChatText.getValue()) {
                    msgBackupIdx--;

                    if (msgBackupIdx <= 0){
                        msgBackupIdx = msgBackupCount;
                    }

                    return ctrlUp();
                } else {
                    $scope.enterChatText.setValue(msg.Msg);
                }
            }

            msgBackupIdx--;

            if (msgBackupIdx <= 0){
                msgBackupIdx = msgBackupCount;
            }
        }
    }

    function ctrlDown() {
        if (sendMsgBackup[frameID]){
            if (msgBackupIdx >= msgBackupCount){
                msgBackupIdx = 0;
            }

            var msg = sendMsgBackup[frameID][msgBackupIdx];

            msgBackupIdx++;

            if (msg && msg.Msg) {
                if (msg.Msg === $scope.enterChatText.getValue()) {
                    ctrlDown();
                } else {
                    $scope.enterChatText.setValue(msg.Msg);
                }
            }
        }
    }

    function _getUinWith(msg){
        return ((mcConst.UserInfo.UIN == msg.UINFrom) ? msg.UINTo : msg.UINFrom);
    }

    function addMsgToBackup(msg){
        if (!msg.History && msg.Mod !== mcConst._CMD_.msgMods.DELETED &&
            (msg.UINFrom == mcConst.UserInfo.UIN || msg.UIN == mcConst.UserInfo.UIN)){
            var frame = msg.UID ? $rootScope.createUID(msg.UID) : $rootScope.createUIN(_getUinWith(msg));
            var idx   = 0;

            if (!sendMsgBackup[frame]){
                sendMsgBackup[frame] = [];
            }

            sendMsgBackup[frame].forEach(function (_msg, _idx){
                if (_msg.Msg == msg.Msg) {
                    idx = _idx;
                }
            });

            if (sendMsgBackup[frame].length >= msgBackupCount || (sendMsgBackup[frame][idx] && sendMsgBackup[frame][idx].notSend)){
                sendMsgBackup[frame].splice(idx, 1);
            }

            sendMsgBackup[frame].push(msg);

            msgBackupIdx = msgBackupCount;
        }
    }
    
    function allBackupMsgSended(msg) {
        if (!msg.History && msg.Mod !== mcConst._CMD_.msgMods.DELETED &&
            (msg.UINFrom == mcConst.UserInfo.UIN || msg.UIN == mcConst.UserInfo.UIN)) {
            var frame = msg.UID ? $rootScope.createUID(msg.UID) : $rootScope.createUIN(_getUinWith(msg));

            sendMsgBackup[frame] && sendMsgBackup[frame].forEach(function (_msg, _idx){
                _msg.notSend = false;
            });
        }
    }
    

    // =====================================

    function stopDrawTypingNotify(){
        clearTimeout(timerTypingNotify);

        timerTypingNotify = null;

        if (mainView && mainView.className && mainView.className.indexOf('noBG') !== -1) {
            mainView.className = mainView.className.replace(' noBG', "");
        }

        $scope.PrivateTypingTextNotify.define('template', ' ');
        $scope.PrivateTypingTextNotify.refresh();
    }

    function startDrawTypingNotify(){
        if (mainView && mainView.isVisible()){
            var notifyView = mcService.getFrame('framePrivateTypingNotify');

            if (notifyView && notifyView.className.indexOf('noBG') === -1) {
                notifyView.className += ' noBG';
            }

            $scope.PrivateTypingTextNotify.define('template', mcService.insertIco("fa-pencil") + mcService.Lang(49) + "&nbsp;&nbsp;"); // "49" : "пишет...",
            $scope.PrivateTypingTextNotify.refresh();
        }
    }

    // =====================================

    $scope.startVoiceMessage = function() {
        $scope.clipPopupWindow.hide();

        mcVoiceRecorder.startRecording();
    };

    // =====================================

    $scope.scrollDown = function () {
        scrollDown(frameID, false, true);
    };

    $scope.showContextUserMenu = function(){

    };

    $scope.hideContextUserMenu = function(){
        contextUser = null;
    };

    $scope.clearHistoryContext = function(){
        this.getTopParentView().hide();

        switch ($rootScope.GetChatType()) {
            case $rootScope.chatAliases.UID:
                $rootScope.$broadcast(window._messages_.dialogsList.clearConfHistory, [$rootScope.GetChatID(), 40]);
            break;

            case $rootScope.chatAliases.UIN:
                $rootScope.$broadcast(window._messages_.dialogsList.clearPrivHistory, [$rootScope.GetChatID(), 40]);
            break;
        }
    };

    $scope.closePrivateContext = function () {
        this.getTopParentView().hide();

        $rootScope.$broadcast('removeDialogFromList', [$rootScope.GetChatID()]);
    };

    $scope.drawSmiles = function(){
        var res = [];
        var smilesFiles = [];

        /*if (mcConst.ClientSettings.SysEventsDisableEmotions == '0'){
            Object.keys(mcConst.SmileysSet[$rootScope.smileSetNumb]).forEach(function (sName) {
                var sFile = mcConst.SmileysSet[$rootScope.smileSetNumb][sName];

                if (sName !== "name") {
                    if (smilesFiles.indexOf(sFile) === -1){
                        smilesFiles.push(sFile);

                        res.push(mcService.myReplaceFormated(
                            "<img src='#{path}/#{smile}' class='_smileImage_' title='#{title}'>",
                            {
                                path : MC_RESOURCE.TextSource.smileys + mcConst.SmileysSet[$rootScope.smileSetNumb].name,
                                smile: sFile,
                                title: sName
                            }
                        ));
                    }
                }
            });
        }*/

        var smm = mcEmojiList.getAllEmoji();

        smm.forEach(function (_smile) {
            var smile = _smile.split('|');

            // res.push("<span class='finger' style='font-size: 24px; width: 33px; display: inline-block' title='" + smile[1] + "'>" + smile[0] + "</span>");
            res.push(mcService.myReplaceFormated(
                "<span class='finger inlineBlock' title='#{title}' emoji='#{code}'>" +
                "<img src='#{src}' class='emojiSize' emoji='#{code}'>" +
                "</span>",
                {
                    title: smile[1],
                    code : smile[0],
                    src  : MC_RESOURCE.TextSource.smileys + "emoji/" + smile[0].replace("&#x","") + ".png"
                }
            ));
        });

        return "<div style='overflow-y: auto; height: 100%; padding: 3px'>" + res.join(" ") + "</div>";
    };

    $scope.inChatClick = {
        "noImageClick"       : function (ev) {
            var element  = ev.target;
            var hash     = element.getAttribute("hash");
            var fileName = element.getAttribute("fileName");
            var idx      = element.getAttribute("msgIdx");

            downloadUploadManager.loadThumbs([
                hash,
                fileName,
                idx
            ]);

            ev.preventDefault();
        },

        "openFolder"         : function openFolder (ev){
            // var path = ev.target.getAttribute('filepath') || ev.target.parentElement.childNodes[1].getAttribute('filepath');
            var path = ev.target.getAttribute('filepath') || element.parentElement.querySelector(".openFolder").getAttribute('filepath');

            downloadUploadManager.openFolder(path);
        },

        "deleteMessage"      : function (ev){
            var trash  = ev.target;
            var idx    = trash.getAttribute('Idx');
            var uinUID = getCurrentID();
            var elm    = "";
            var param  = "";
            var parent = trash.parentElement;

            ev.preventDefault();

            if (uinUID && getCurrentType() === $rootScope.chatAliases.UID){
                elm   = trash.parentElement.getElementsByClassName("messageText")[0];
                param = mcService.copy(elm.innerText, 0, 20) + (elm.innerText.length > 20 ? "..." : "");

                if (!param){
                    param = "<div class='adjustImg'>" + elm.innerHTML + "</div>";
                }

                webix.confirm({
                    type  : "confirm-warning",
                    text  : mcLang(587, param),  // "587":"Удалить сообщение",
                    ok    : mcService.Lang(519), // "519":"Удалить",
                    cancel: mcService.Lang(33),  // "33" :"Отмена",

                    callback:function(yes){
                        _deleteMessage(yes, uinUID, idx, mcConst._CMD_.cs_moder_delete_conf_message, trash, parent);
                    }
                });
            } else
            if (uinUID && getCurrentType() === $rootScope.chatAliases.UIN) {
                elm   = trash.parentElement.getElementsByClassName("messageText")[0];
                param = mcService.copy(elm.innerText, 0, 20) + (elm.innerText.length > 20 ? "..." : "");

                if (!param){
                    param = "<div class='adjustImg'>" + elm.innerHTML + "</div>";
                }

                webix.confirm({
                    type  : "confirm-warning",
                    text  : mcLang(587, param),  // "587":"Удалить сообщение",
                    ok    : mcService.Lang(519), // "519": "Удалить",
                    cancel: mcService.Lang(33),  // "33": "Отмена",

                    callback:function(yes){
                        _deleteMessage(yes, uinUID, idx, mcConst._CMD_.cs_delete_private_message, trash, parent);
                    }
                });
            }
        },

        "openOrDownloadFile" : function openOrDownloadFile (ev){
            var element  = ev.target;
            var url      = element.href;
            var fileInfo = {
                uin        : element.getAttribute("uin"),
                displayName: element.getAttribute("nick"),
                filePath   : element.getAttribute("filepath"),
                fileDT     : element.getAttribute("filedt"),
                hash       : element.getAttribute("hash")
            };

            if ($rootScope.isWebClient){
                // window.open(url);
            } else {
                ev.preventDefault();

                downloadUploadManager.openDownloadUrl(url, fileInfo);
            }

            return false;
        },

        "downloadFile"       : function downloadFile (ev){
            var element  = ev.target;
            // var link     = element.parentElement.childNodes[1];
            var link     = element.parentElement.querySelector("a");
            var url      = link.href;
            var fileInfo = {
                uin        : link.getAttribute("uin"),
                displayName: link.getAttribute("nick"),
                filePath   : link.getAttribute("filepath"),
                fileDT     : link.getAttribute("filedt"),
                hash       : link.getAttribute("hash")
            };

            if ($rootScope.isWebClient){
                window.open(url);
            } else {
                downloadUploadManager.downloadUrl(url, fileInfo);
            }
        },

        "openInFolder"       : function openInFolder (ev){  // todo: remove
            var element  = ev.target;
            var link     = element.parentElement.querySelector("a");
            var hash     = link.getAttribute('hash');
            var uin      = link.getAttribute('uin');
            var path     = element.getAttribute('filepath') || link.getAttribute('filepath');

            downloadUploadManager.openInFolder(hash, uin, path);
        },

        "_messageUserName"   : function replyToUser(ev){
            var element = ev.target;
            var uin     = element.getAttribute('uin');

            if (uin != mcConst.UserInfo.UIN){
                var txt = element.innerText;

                cursorPosition = mcService.insertAtCursor($scope.enterChatText.getInputNode(), " @" + txt);

                focusToEnterChat();
            }
        },

        "copyTextBtn"        : function (ev){
            var txt = ev.target.getAttribute('txt');

            if (txt){
                mcService.copyTextToClipboard(txt, mcLang(720)); // "720":"Текст скопирован в буфер обмена",
            }
        },

        "replyMessage"       : function (ev){
            var element = ev.target;
            var msgDIV  = element.parentElement.cloneNode(true);
            var childList = msgDIV.querySelectorAll('*');
            var icon    = document.createElement("span");

            icon.className = "webix_icon fa-reply fAwesome replyMessageEx";

            msgDIV.insertBefore(icon, msgDIV.firstChild);

            replyToMsgIdx = element.getAttribute('Idx');

            $scope.replyBlockWrapper.show();

            if (msgDIV.className.indexOf("msgSeparator") >= 0){
                msgDIV.className = msgDIV.className.replace("msgSeparator", "");
            }

            childList.forEach(function (item){
                if (item.className === "quoteMessage" ||
                    item.className.indexOf("replyMessage") >= 0 ||
                    item.className.indexOf("statusItem") >= 0 ||
                    item.className.indexOf("circleAvatarSmall") >= 0 ||
                    item.className.indexOf("deleteMessage") >= 0){
                    item.parentElement.removeChild(item);
                }

                if (item.tagName === "IMG"){
                    item.style.maxHeight = "28px";
                }

                if (item.className === "emojiSize"){
                    item.style.verticalAlign = "middle";
                }

                if (item.id){
                    item.id += "_reply_";
                }
            });

            var replyDIV = $scope.replyBlock.getNode();

            replyDIV.style.position = "relative";
            replyDIV.innerHTML = "";
            replyDIV.appendChild(msgDIV);

            focusToEnterChat();
        }
    };

    $scope.sendSmile = {
        "finger": function(ev, id){
            // var smile = "L$" + mcConst.terminator2 + ")" + $rootScope.smileSetNumb + mcConst.terminator3 + ev.target.title + mcConst.terminator2;

            // _sendText(null, null, smile, mcConst._CMD_.msgType.OLD_SHIT, old_shit_type.smile);

            // this.getTopParentView().hide();

            var emoji = ev.target.getAttribute("emoji");
            
            cursorPosition = mcService.insertAtCursor($scope.enterChatText.getInputNode(), emoji);

            focusToEnterChat();

            return false; // blocks the default click behavior
        }
    };
    
    $scope.popupInsertPhrase = function(){

    };

    $scope.sendText = _sendText;

    $scope.popupInsertFile = function(){
        this.getTopParentView().hide();

        $scope.uploadFile(mcConst._CMD_.msgType.FILE_NEW, false, false);
    };

    $scope.popupInsertImage = function(){
        this.getTopParentView().hide();

        // $scope.uploadFile(mcConst._CMD_.msgType.IMAGE, false, false);
        $scope.uploadFile(mcConst._CMD_.msgType.IMAGE_NEW, false, false);
    };

    $scope.saveTextInputSize = function () {
        // inputSize = $scope.inputWrapper.$getSize()[3];
        
        $rootScope.Storage.dialogs.save(null, null, mcConst.storageOpts.INPUTOPTS, $scope.inputWrapper.$getSize()[3]);
    };

    $scope.loadTextInputSize = function () {
        $rootScope.Storage.dialogs.load(null, null, mcConst.storageOpts.INPUTOPTS, function (height) {
            if (height) {
                $scope.inputWrapper.define('height', height < mcConst.minEnterTextHeight ? mcConst.minEnterTextHeight : height);
                $scope.inputWrapper.resize();
                $scope.inputWrapper.resizeChildren();
            }
        });
    };

    $scope.closeReplyMsg = function (){
        replyToMsgIdx = 0;
        $scope.replyBlockWrapper.hide();
    };

    // === REGISTER HOT KEYS ===

    $scope.registerHotKeys = function () {
        // var tempList = (new Array(9)).join('.').split('.');
        //
        // tempList.forEach(function (val, key) {
        //     tempList[key] = {
        //         key   : mcConst.keyCodes["n" + (key + 1)],
        //         altKey: true,
        //         func  : function () {
        //             $scope.sendQuickMsg(key + 1);
        //         }
        //     };
        // });

        $rootScope.hotKeyDispatcher.addPreset(mcConst.dataModels.ChatFrame, /*tempList.concat(*/[{
            key   : mcConst.keyCodes.end,
            ctrlKey: true,
            func  : function () {
                return true;
            }
        }, {
            key   : mcConst.keyCodes.enter,
            func  : function () {
                $scope.sendText();
            }
        }, {
            key   : mcConst.keyCodes.enter,
            ctrlKey: true,
            func  : function () {
                if ($rootScope.isWebClient || mcConst.ClientSettings.GeneralCtrlEnterSend) {
                    $scope.sendText();
                } else {
                    mcService.insertAtCursor($scope.enterChatText.getInputNode(), "\n");
                }
            }
        }, {
            key   : mcConst.keyCodes.f8,
            func  : function () {
                var node = $scope.enterChatText.getInputNode();
                var selectedText = mcService.getSelectedText(node);

                if (selectedText.infix){
                    $scope.enterChatText.setValue(selectedText.prefix + transformRusText(selectedText.infix) + selectedText.postfix);

                    mcService.setSelectionText(node, selectedText.selectionStart, selectedText.selectionEnd);
                } else {
                    $scope.enterChatText.setValue(transformRusText($scope.enterChatText.getValue()));
                    
                    mcService.setCaretPosition(node, selectedText.selectionEnd);
                }
            }
        }, {
            key   : mcConst.keyCodes.aup,
            altKey: true,
            func  : ctrlUp
        }, {
            key   : mcConst.keyCodes.adown,
            altKey: true,
            func  : ctrlDown
        }]/*)*/, $scope.enterChatText.getNode());

        $rootScope.hotKeyDispatcher.addPreset(mcConst.dataModels.ChatFrame + "global", [{
            key   : mcConst.keyCodes.end,
            ctrlKey: true,
            func  : function () {
                if (mainView.isVisible()){
                    scrollDown(frameID, false, true);
                }
            }
        },{
            key   : mcConst.keyCodes.f5,
            func  : function () {
                if (mainView.isVisible()){
                    $scope.clipPopupWindow.hide();

                    $scope.uploadFile(mcConst._CMD_.msgType.FILE_NEW, false, false);
                }
            }
        },{
            key   : mcConst.keyCodes.f6,
            func  : function () {
                if (mainView.isVisible() && $scope._rights.QAllowRedirectDialogs) {
                    $rootScope.$broadcast(window._messages_.privateInfo.redirectUserTo);
                }
            }
        },{
            key   : mcConst.keyCodes.f9,
            ctrlKey: true,
            func  : function () {
                if ($scope._rights.QBroadcasts){
                    // $rootScope.$broadcast(window._messages_.dialogsCtrl.selectDialog, [mcConst.dataModels.SendBroadcast]);
                }
            }
        }, {
            key   : mcConst.keyCodes.f,
            ctrlKey: true,
            func  : function () {
                $scope.searchWrapper.show();
                $scope.searchChatText.focus();
            }
        }, {
            key     : mcConst.keyCodes.v,
            shiftKey: true,
            ctrlKey : true,
            func    : function () {
                if ($scope._rights.QFilesSend && $rootScope.GetChatType() === $rootScope.chatAliases.UIN) {
                    directDownloadUpload.sendFilesToUser(true);
                }
            }
        }, {
            key     : mcConst.keyCodes.f4,
            ctrlKey : true,
            func    : function () {
                $rootScope.$broadcast(window._messages_.dialogsList.removeDialogFromList, [$rootScope.GetChatID()]);
            }
        }, {
            key     : mcConst.keyCodes.f,
            ctrlKey : true,
            shiftKey: true,
            func    : function () {
                if ($scope._rights.QAllowFindUsersTool) {
                    $rootScope.$broadcast(window._messages_.chatWrapper.showGlobalSearch);
                }
            }
        }], document);

        $rootScope.hotKeyDispatcher.addPreset(mcConst.dataModels.ChatFrame + "find", [{
            key   : mcConst.keyCodes.enter,
            func  : function () {
                $rootScope.$broadcast("find_chat_text", [currentFrame, $scope.searchChatText.getValue(), $scope.searchChatText]);
            }
        }, {
            key     : mcConst.keyCodes.esc,
            lockPrev: true,
            func    : $scope.hideFindChatText
        }], $scope.searchChatText.getNode());
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset(mcConst.dataModels.ChatFrame);
        $rootScope.hotKeyDispatcher.removePreset(mcConst.dataModels.ChatFrame + "global");
        $rootScope.hotKeyDispatcher.removePreset(mcConst.dataModels.ChatFrame + "find");
    };

    // =========================

    $scope.hideFindChatText = function () {
        $scope.searchWrapper.hide();
        $scope.searchChatText.setValue("");

        if (!$rootScope.isWebClient) {
            $rootScope.$broadcast("find_clear", [currentFrame]);
        }

        setTimeout(function () {
            $scope.enterChatText.focus();
        }, 50);
    };

    $scope.titleMenu = function(){
        switch ($rootScope.GetChatType()){
            case $rootScope.chatAliases.UIN:
                // $rootScope.$broadcast("selectTool", [mcConst.dataModels.PrivateInfo,  true]);
            break;

            case $rootScope.chatAliases.UID:
                // $rootScope.$broadcast("selectTool", [mcConst.dataModels.ConfUserList, true]);
            break;
        }
    };

    $scope.changeScrollPosition = function () {
        var scrView   = $scope.chatTextOutputW.getNode();
        var newHeight = scrView.scrollHeight;
        var delta     = newHeight - prevHeight;

        if ( delta >= 0){
            scrView.scrollTop += delta;
        } else {
            scrView.scrollTop -= delta;
        }

        prevHeight = newHeight;
    };

    $scope.typingNotify = function(code, e){ // code, e
        if (e.keyCode !== void(0)) {
            code = e.keyCode;
        } else
        if (e.keyIdentifier !== void(0)) {
            code = e.keyIdentifier;
        } else
        if (e.key !== void(0)) {
            code = mcService.isString(e.key) ? e.key.charCodeAt(0) : e.key;
        }

        if (!e.altKey && !e.ctrlKey){
            if (mcConst.keyCodes.PrintSymbols.indexOf(code) !== -1){
                if ($rootScope.isWebClient || mcConst.ClientSettings.SoundsSndChatType) {
                    mcPlaySound.PlaySound(mcPlaySound.Sounds.Typing);
                }
            } else
            if ((code === mcConst.keyCodes.delete || code === mcConst.keyCodes.backspace) && $scope.enterChatText.getValue() && ($rootScope.isWebClient || mcConst.ClientSettings.SoundsSndChatBS)) {
                mcPlaySound.PlaySound(mcPlaySound.Sounds.DeleteAct);
            }
        }

        if ($rootScope.GetChatType() === 'UIN'){
            pressNotifyCount ++;

            if (pressNotifyCount === 10){
                pressNotifyCount = 0;

                $rootScope.$broadcast('SendCMDToServer', [
                    mcConst._CMD_.cs_typing_notify,
                    mcConst.SessionID,
                    $rootScope.GetChatID()
                ])
            }
        }

        setTimeout(function () {
            saveCursorPositionAndMsgTemplate(code, false, $scope.enterChatText.getValue(), frameID);
        }, 10);
    };

    $scope.getUniBadge = function(){
        var bdg = document.createElement("span");
        bdg.className = 'badge_tool toRight hidden';
        bdg.id = '_uniBadge';
        bdg.innerText = " ";
        bdg.style.right = "6px";

        return bdg;
    };

    $scope.show = function(){
        if (!mainView){
            currentSmilesSet  = $rootScope.smileSetNumb;

            $scope.uploadFile = downloadUploadManager.uploadFile;
            $scope.duViews    = downloadUploadManager.getViews();
            $scope.dirDuViews = directDownloadUpload .getViews();
            $scope.callButtons= $rootScope.$broadcast(window._messages_.callManager.getCallBtns);
            
            downloadUploadManager.setPastePreviewWindow(imagePastePreview);
            downloadUploadManager.setPastePreviewWindowFile(filePastePreview);

            mainView = initChatFrame($scope);

            $rootScope.$broadcast(window._messages_.callManager.initCallBtns);

            framesList.initView($scope.chatTextOutput);

            currentFrame = framesList.create("UIN-0");

            downloadUploadManager.onPasteItemFromClipboard($scope.enterChatText.$view);

            $scope.chatTextOutputW.getNode().onmousedown = function(e){
                if ((this.clientWidth + this.offsetLeft) <= e.clientX) {
                    $scope.chatTextOutputW.getNode().onmouseup = function(){
                        $scope.chatTextOutputW.getNode().onmouseup = void(0);
                    };
                }
            };

            $scope.registerHotKeys();

            // -- DownloadUpload --
            downloadUploadManager.initViews();
            directDownloadUpload .initViews();

            deferredLoadImages = new McDeferredLoadImages();

            // $scope.getPhraseList(mcConst.UserInfo.Templates);
        } else {
            mainView.show();
        }

        // if (currentSmilesSet !== $rootScope.smileSetNumb){
            $scope.smilesWindowTemp.define("template", $scope.drawSmiles());
            $scope.smilesWindowTemp.refresh();
        // }

        switch ($rootScope.GetChatType()) {
            case $rootScope.chatAliases.UIN:
                $scope.centerSpace.getNode().className = "webix_view noMargin gradient webix_layout_line boxShadow";
            break;

            case $rootScope.chatAliases.UID:
                $scope.centerSpace.getNode().className = "webix_view noMargin gradient webix_layout_line boxShadow";
            break;

            default:
                $scope.centerSpace.getNode().className = "webix_view noMargin webix_layout_line boxShadow";
        }

        if (frameID) {
            framesList.get(frameID, noCreate).show();
        }

        downloadUploadManager.canStart();

        getOperatorsList(function () {
            if ($rootScope.initialParams.select && $scope.operList.length){
                // console.log("добавить выбор оператора!!");
                // $scope.selectNewOperator.show();
                // $scope.displayName.hide();
                //
                // fillOpersList();
            }
        });

        resizeMainWindow($rootScope.wndSize.width, $rootScope.wndSize.height);
    };

    //========================================

    var _msg = window._messages_.chatFrame = {
        showMirrorVideoFrame    : 'showMirrorVideoFrame',
        blurChatTextOutput      : 'blurChatTextOutput',
        showVideoFrame          : 'showVideoFrame',
        hideVideoFrame          : 'hideVideoFrame',
        // clearChatFrame          : 'clearChatFrame',
        fillChatFrame           : 'fillChatFrame',
        addChatMessage          : 'addChatMessage',
        addCustomMSG            : 'addCustomMSG',
        clearConfMessages       : 'clearConfMessages',
        clearPrivateMessages    : 'clearPrivateMessages',
        removeFrameChat         : 'removeFrameChat',
        on_mainWindowFocused    : 'on_mainWindowFocused',
        focusToEnterChat        : 'focusToEnterChat',
        scrollDownChat          : 'scrollDownChat',
        showNotifyMessage       : 'showNotifyMessage',
        updateMessagesState     : 'updateMessagesState',
        on_private_got_notify   : 'on_private_got_notify',
        on_private_read_notify  : 'on_private_read_notify',
        quickMessagesList       : 'quickMessagesList',
        onTypingNotify          : 'onTypingNotify',
        syncHistoryDone         : 'syncHistoryDone',
        updateFrameConfCount    : 'updateFrameConfCount',
        setUniBadge             : 'setUniBadge',
        hideUniBadge            : 'hideUniBadge',
        restoreMessage          : 'restoreMessage',

        '----':'----'
    };

    // ===================================================================

    $scope.$on(_msg.restoreMessage, function (){
        if (sendMsgBackup[frameID] && sendMsgBackup[frameID].length) {
            var msg = sendMsgBackup[frameID][sendMsgBackup[frameID].length - 1];

            if (msg.notSend && $scope.enterChatText) {
                $scope.enterChatText.setValue(msg.Msg);
            }
        }
    });

    $scope.$on(_msg.setUniBadge, function (e, args){
        _uniBadgeCount = args[0];

        var bdg = document.getElementById("_uniBadge");

        if (!bdg){
            bdg = $scope.getUniBadge();
            // $scope.hideMainMenu.getNode().appendChild(bdg);
        }

        if (_uniBadgeCount && menuIsHidden){
            mcService.showView(bdg);
        } else {
            mcService.hideView(bdg);
        }
    });

    // $scope.$on(_msg.hideUniBadge, function (){
    //     $scope.hideMainMenu.setValue(0);
    // });

    $scope.$on(_msg.updateFrameConfCount, function (e, args){
        updateFrameConfCount.apply(null, args);
    });

    $scope.$on(_msg.syncHistoryDone, function (e, args){
        hideLoadHistoryFrame.apply(null, args);
    });

    // $scope.$on(_msg.quickMessagesList, function (e, args){
        // $scope.getPhraseList.apply(null, args);
    // });

    $scope.$on(_msg.updateMessagesState, function (e, args) {
        var UIN        = args[0];
        var fUIN       = $rootScope.createUIN(UIN);
        var myMessages = drawMessage.getMyMessagesTemp();

        if (UIN != mcConst.UserInfo.UIN && $rootScope.GetChatTypeID() === fUIN && myMessages.UIN != UIN) {
            myMessages = mcService.Marge(myMessages, $rootScope.$broadcast(window._messages_.dialogsList.getMyMessagesState, [UIN]));

            if ($rootScope.GetChatTypeID() === fUIN){
                markReadNotify.apply(null, [UIN, myMessages.IDMyMsgRead]);

                if (myMessages.IDMyMsgGot > myMessages.IDMyMsgRead){
                    markGotNotify.apply(null, [UIN, myMessages.IDMyMsgGot]);
                }
            }

            drawMessage.setMyMessages(myMessages);
        }
    });

    $scope.$on(_msg.on_private_got_notify, function (e, args) {
        markGotNotify.apply(null, [args[0].UIN, args[0].ID]);
    });

    $scope.$on(_msg.on_private_read_notify, function (e, args) {
        markReadNotify.apply(null, [args[0].UIN, args[0].ID]);
    });

    $scope.$on(_msg.showNotifyMessage, function (e, args) {
        drawMessage.showNotify.apply(null, args);
    });

    $scope.$on(_msg.scrollDownChat, function (e, args) {
        scrollDown.apply(null, args);
    });

    $scope.$on(_msg.focusToEnterChat, function (e, args) {
        focusToEnterChat.apply(null, args);
    });

    $scope.$on(_msg.on_mainWindowFocused, function(){
        focusToEnterChat();
    });

    $scope.$on(_msg.showMirrorVideoFrame, function(){
        mcService.showFrame(mcConst.myVideoStream);
    });

    $scope.$on(_msg.blurChatTextOutput, function(){
        $scope.clipPopupWindow.hide();

        $scope.enterChatText.blur();
    });

    $scope.$on(_msg.showVideoFrame, function(e, args){
        showVideoFrame.apply(null, args);
    });

    $scope.$on(_msg.hideVideoFrame, function (e, args) {
        hideVideoFrame.apply(null, args);
    });

    $scope.$on(_msg.fillChatFrame, function(e, args){
        fillFrame.apply(null, args);
        
        $scope.hideFindChatText();
    });

    $scope.$on(_msg.addChatMessage, function(e, args){
        if ($scope.chatTextOutputW) {
            var msg = args[0];

            if (msg.UINFrom == $rootScope.GetChatID()) {
                stopDrawTypingNotify();
            }

            drawMessage.chatMessage(msg, currentFrame, $scope.chatTextOutputW.getNode());

            allBackupMsgSended(msg)
        } else {
            console.error('$scope.chatTextOutputW not found');
        }
    });

    $scope.$on(_msg.addCustomMSG, function(e, args){
        drawMessage.customMessage(args[0], currentFrame, $scope.chatTextOutputW.getNode());
    });

    $scope.$on(_msg.clearConfMessages, function(e, args){
        var uid     = args[0];
        var delList = args[1];
        var _frameID= "UID-" + uid;
        var conf    = framesList.get(_frameID, noCreate);

        if (conf){
            conf.hide();

            drawMessage.removeMsgList(conf, uid, delList, "conf-");

            conf.show();
        }

        scrollDown(_frameID, false, true);
    });

    $scope.$on(_msg.clearPrivateMessages, function(e, args){
        var uin     = args[0];
        var delList = args[1];
        var _frameID = "UIN-" + uin;
        var priv    = framesList.get(_frameID, noCreate);

        if (priv){
            priv.hide();

            drawMessage.removeMsgList(priv, uin, delList, "priv-");

            priv.show();
        }

        scrollDown(_frameID, false, true);
    });

    $scope.$on(_msg.removeFrameChat, function (e, args) {
        var type = args[0];
        var idx  = args[1];
        
        framesList.remove(type + "-" + idx);
    });

    $scope.$on(_msg.onTypingNotify, function(e, args){
        var uin = args[0].UIN;

        if (uin == $rootScope.GetChatID() && $rootScope.GetChatType() === "UIN"){
            if (!timerTypingNotify){
                startDrawTypingNotify();

                timerTypingNotify = setTimeout(function(){
                    stopDrawTypingNotify();
                }, 4000);
            }
        }
    });

    $scope.$on('hide' + $scope.Name, function(){
        $scope.hideFindChatText();

        framesList.hide(frameID, function () {
            // var msg = $scope.enterChatText.getValue();
            //
            // msg && saveMsgTemplate(msg, frameID, true);
        });

        mainView.hide();

        downloadUploadManager && downloadUploadManager.noStart();
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        $scope.show();

        $scope.loadTextInputSize();
    });

    $rootScope.$broadcast('windowResize', [function(w, h){
        if (mainView && mainView.isVisible()){
            var inFocus = webix.UIManager.getFocus();

            if (inFocus && inFocus.config.id === $scope.enterChatText.config.id){
                $scope.enterChatText.focus();
            }

            resizeMainWindow(w, h);
        }
    }]);

    $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
        $scope._rights.QBroadcasts              = mcService.checkRights(mcConst._CMD_.RS.QBroadcasts);
        $scope._rights.QFilesSend               = mcService.checkRights(mcConst._CMD_.RS.QFilesSend);
        $scope._rights.QAllowFindUsersTool      = mcService.checkRights(mcConst._CMD_.RS.QAllowFindUsersTool);
        $scope._rights.QSendImages              = mcService.checkRights(mcConst._CMD_.RS.QSendImages);
        $scope._rights.QImagesPrivates          = mcService.checkRights(mcConst._CMD_.RS.QImagesPrivates);
        $scope._rights.QTxtConfSendFiles        = mcService.checkRights(mcConst._CMD_.RS.QTxtConfSendFiles);
        $scope._rights.QPrivInsertFiles         = mcService.checkRights(mcConst._CMD_.RS.QPrivInsertFiles);
        $scope._rights.QCommonAllowQuickMessages= mcService.checkRights(mcConst._CMD_.RS.QCommonAllowQuickMessages);
        $scope._rights.QAllowRedirectDialogs    = mcService.checkRights(mcConst._CMD_.RS.QAllowRedirectDialogs);
        $scope._rights.QAllowViewRemoteUserProfile = mcService.checkRights(mcConst._CMD_.RS.QAllowViewRemoteUserProfile);

        attachButtons();
    });
}

function mainController($scope, $rootScope, mcCmdParser, /*mcCmdLoop,*/ $http, $location, mcStatistics, mcConnect){
    "use strict";

    // ===================================================================

    window.idMcPingTimer = null;

    var pingOptions =  {
        onError : null,
        onStop  : null,
        onWarn  : null
    };

    function StartPingTimer(_time, opt){
        pingOptions = opt;

        if (mcConst.SessionID) {
            window.idMcPingTimer = setInterval(function(){
                if (mcConst.SessionID) {
                    $rootScope.SendCMDToServer([mcConst._CMD_.Ping, mcConst.SessionID]);
                } else {
                    console.error("mcConst.SessionID is not defined. StopPingTimer()");

                    StopPingTimer();
                }
            }, _time);
        }

        function RunCMD(CMD_Block, debug) {
            if (debug) {
                console.log("CMD_Block:\n" + CMD_Block);
            }

            if (CMD_Block && CMD_Block.length > 0){
                CMD_Block = mcService.StringToObj(CMD_Block);

                for (var CMD_Numb in CMD_Block){
                    $rootScope.ProcessCMD([CMD_Numb, CMD_Block[CMD_Numb]]);
                }
            }
        }

        mcConnect.ClearAllCallBackFunctionByCMD();
        mcConnect.LeaveCallBacks = true;

        mcConnect.SetCallBackFunctionByCMD(mcConst._CMD_.Ping, function(){
            var data = this;

            if ((data !== "{}") && (data.length > 0)) {
                RunCMD(data);
            }
        });

        mcConnect.OnError = function(err){
            if (console) {
                console.warn('An error occurred: Ping-Timer has been Stopped');
            }

            StopPingTimer();

            if (pingOptions.onError){
                pingOptions.onError(err);
            }
        };

        return window.idMcPingTimer;
    }

    function StopPingTimer(){
        if (window.idMcPingTimer !== null){
            clearInterval(window.idMcPingTimer);

            window.idMcPingTimer = null;

            mcConnect.ClearAllCallBackFunctionByCMD();

            if (pingOptions.onStop) {
                pingOptions.onStop();
            }
        }
    }

    // ===================================================================

    function initLockWindow(closeBtn, cb){
        if (lockWindow){
            lockWindow.destructor();
        }

        lockWindow = webix.ui({
            view     : "window",
            modal    : true,
            head     : mcService.Lang(18), // "18" : "Обработка запроса",
            position : "center",
            width    : 220,
            // height   : mcConst.lockWindowHeight + (closeBtn ? 100 : 0),
            body     : {
                height: mcConst.lockWindowHeight + (closeBtn ? 32 : 0),
                rows: [
                    { template : "<div align='center'><img src='" + mcConst.imagesPath.loading + "'></div>", borderless: true, autoheight: true },
                    { hidden: !closeBtn, cols: [ {}, { view: "button", value: mcLang(33), width: 100, click: function () { // "33" :"Отмена",
                        lockWindow.hide();

                        $rootScope.drodConnectOnLogin = true;

                        cb && cb();
                    }}, {}] }
                ]
            }
        });
    }

    function displayError(txtCode, txt, txtParam, callback, okText){
        if (!errorDialog) {
            errorMessagesIsShowed = true;

            lockWindow && lockWindow.hide();
            
            errorDialog = webix.alert({
                title    : txtCode === -1 ? "" : mcService.Lang(11, txtCode), // mcService.Lang(11, err.ErrNum), // "11" : "Ошибка #"
                ok       : okText || mcService.Lang(12),          // "12" : "Закрыть",
                type     : "alert-error",
                width    : 400,
                text     : mcService.myReplace(txt, txtParam),
                callback : function () {
                    if (callback) {
                        callback();
                    }

                    $rootScope.lastError = txtCode;

                    var args = errorMessages.shift();

                    if (args !== undefined) {
                        setTimeout(function () {
                            displayError.apply(null, args);
                        }, 20);
                    }

                    errorMessagesIsShowed = false;

                    errorDialog = null;
                }
            });
        }
    }

    function sendCMD(args, toElectron){
        if (args[0] === mcConst.lockInterface){
            args.shift();

            var isLogin = mcService.inArray(args[0], [mcConst._CMD_.Login, mcConst._CMD_.cs_login_by_token, mcConst._CMD_.Register]) >= 0;

            if ($rootScope.isWebClient) {
                $scope.$emit(mcConst.lockInterface);
            } else {
                // $scope.$emit(mcConst.lockInterface, [isLogin, isLogin && function () {
                //     console.log('close connect');
                //
                //     mcCmdParser.internalSender([
                //         mcConst._CMD_.ce_drop_connect
                //     ]);
                // }]);
            }
        }

        if (mcCmdParser.addCustomCbCMD.apply(null, args)) {
            args.pop();
        }

        // if (toElectron){
        //     mcCmdParser.internalSender(args);
        // } else {
            if (mcStatistics){
                mcStatistics.setValueByCMD.apply(null, args);
            }

            if (mcCmdParser.MultiSender.apply(null , args)){
                mcCmdParser.runCmd(true, args); // true = SendCMDToServer
            }
        // }
    }

    function setWindowSize() {
        $rootScope.wndSize = mcService.getWindowSize();

        for (var i = 0; i < resizeWndCb.length; i++){
            resizeWndCb[i]($rootScope.wndSize.width, $rootScope.wndSize.height);
        }
    }

    function receiveCMD(args) {
        if (mcStatistics){
            mcStatistics.setValueByCMD.apply(null, args);
        }

        if (mcCmdParser.MultiParser.apply(null, args)){
            // mcCmdParser.runCmd('ProcessCMD', args);
            mcCmdParser.runCmd(false, args); // false = ProcessCMD
        }
    }

    function startPingTimer (){
        mcConst.PingTimer = StartPingTimer(mcConst.PingInterval, /*mcConst.SessionID,*/ {
            onError : function(){
                if ($rootScope.isWebClient){
                    mcConst.LoggedIn = false;

                    $rootScope.$broadcast('ErrorMsg', ['223', mcService.Error('223'), '', function(){
                        location.search = "";
                        location.reload();
                    }]);
                } else {
                    $rootScope.restoreConnection = false;

                    $rootScope.$broadcast('ReconnectLoop');

                    $rootScope.$broadcast('ErrorMsg', ['223', mcService.Error('223') + "<br>" + mcLang(539) + "<br><br>" + mcConst.loadGif, '', function(){ // "539":"Восстанавливаем связь!",
                        mcService.ClearSessionSettings(true, true);

                        location.search = "";
                        location.reload();
                    }, mcLang(540)]); // "540":"Прервать",
                }
            },
            onStop: function () {
                mcConst.LoggedIn  = false;
                mcConst.PingTimer = null;
                mcConst.SessionID = '';

                $rootScope.destroySocketIo();

                if (!$rootScope.isWebClient){
                    $rootScope.$broadcast("disableGlobalHotKeys");
                }
            }
        });
    }

    // ========= Отправка клиентских логов (browser) на сервер ===========

    if (window.cLog){
        cLog(mcConst._CMD_.ce_console_log, sendCMD);
    }

    // ===================================================================

    var aliases        = {
        Elisa : "UIN-0",
        Conf  : mcConst.dataModels.ConfUserList,
        UIN   : "UIN",
        UID   : "UID",
        Forum : mcConst.dataModels.Forum,
        Privat: mcConst.dataModels.PrivateInfo,
        Kanban: mcConst.dataModels.Kanban,
        Admin : mcConst.dataModels.Admin,
        BBS   : mcConst.dataModels.BBS,
        SendBroadcast     : mcConst.dataModels.SendBroadcast,
        HistoryBroadcast  : mcConst.dataModels.HistoryBroadcast
    };
    var CurrentDialog  = {
        id    : '-1',
        type  : aliases.UIN,
        alias : mcConst.dataModels.PrivateInfo
    };
    var lockWindow     = null;
    var reconnectTimer = null;
    var resizeWndCb    = [];
    var toolShowed     = null;
    var rightCheck     = { /* where: function() */ };
    var lockCount      = 0;
    var lockTimer      = null;
    var errorDialog    = null;
    var errorMessages  = [];
    var errorMessagesIsShowed = false;

    // ====================================================

    if (window.McTextFinder) {
        McTextFinder($rootScope);
    }

    // ====================================================

    $rootScope.hotKeyDispatcher  = new mcService.HotkeyManager(); // true - debug on
    $rootScope.restrictedSymbols = '"\/\\[]:;|=,+*?<>';
    $rootScope.customUserOptions = null;
    $rootScope.ErrorActions      = {};
    $rootScope.ElectronErrors    = {};
    $rootScope.acConfig          = { serversInfo: {} };
    $rootScope.thumbsSize        = mcConst.imageSize.big;
    $rootScope.mcStorage         = mcService.Marge({}, mcConst.dataModels);
    $rootScope.needFoto          = false;
    $rootScope.Purl              = purl();
    $rootScope.smileSetNumb      = "1";
    $rootScope.wndSize           = mcService.getWindowSize();
    $rootScope.isWebClient       = location.protocol !== 'file:';
    $rootScope.chatAliases       = aliases;
    $rootScope.SendCMDToServer   = sendCMD;
    $rootScope.ProcessCMD        = receiveCMD;
    $rootScope.drodConnectOnLogin= false;

    mcService._hotkeys = $rootScope.hotKeyDispatcher;

    $rootScope.colorPalette = [
        "FFB900", "E74856", "0078D7", "0099BC", "7A7574", "767676",
        "FF8C00", "E81123", "0063B1", "2D7D9A", "5D5A58", "4C4A48",
        "F7630C", "EA005E", "8E8CD8", "00B7C3", "68768A", "69797E",
        "CA5010", "C30052", "6B69D6", "038387", "515C6B", "4A5459",
        "DA3B01", "E3008C", "8764B8", "00B294", "567C73", "647C64",
        "EF6950", "BF0077", "744DA9", "018574", "486860", "525E54",
        "D13438", "C239B3", "B146C2", "00CC6A", "498205", "847545",
        "FF4343", "9A0089", "881798", "10893E", "107C10", "7E735F"
    ];

    // ====================================================

    if (!$rootScope.isWebClient) {
        try {
            initClientData.apply(null, [$scope, $rootScope, sendCMD, mcStatistics]);
        } catch (e){}
    }

    // ====================================================

    $rootScope.setTool = function(name){
        if (toolShowed !== name) {
            $rootScope.hidePrevTool(toolShowed);

            toolShowed = name;
        }
    };

    $rootScope.getTool = function(){
        return toolShowed;
    };

    $rootScope.hidePrevTool = function(){
        if (toolShowed)
            $rootScope.$broadcast('hide' + toolShowed, []);
    };

    // ======= Error Actions ==========

    $rootScope.ErrorActions[17] = function(params){ // "17"  : "Пользователь с таким UIN (%s) не зарегистрирован на сервере",
        if (mcConst.LoggedIn){
            $rootScope.$broadcast('removeDialogFromList', params);
        } else {
            if (!$rootScope.isWebClient){
                $rootScope.UNIT.ctrlServersManager.setAutoConnectInfo("", false, "");
            }

            $rootScope.$broadcast('show' + mcConst.dataModels.Login, []);
        }
    };

    $rootScope.ErrorActions[11] = $rootScope.ErrorActions[17];

    // ====================================================

    /**
     * @return {string}
     */
    $rootScope.GetChatID     = function(){
        return CurrentDialog.id;
    };

    /**
     * @return {string}
     */
    $rootScope.GetChatType   = function(){
        return CurrentDialog.type;
    };

    $rootScope.GetChatAlias  = function(){
        return CurrentDialog.alias;
    };

    /**
     * @return {string}
     */
    $rootScope.GetChatTypeID = function(){
        return CurrentDialog.type + "-" + CurrentDialog.id;
    };
    // ====================================================

    $rootScope.createUIN  = function(uin){
        return $rootScope.chatAliases.UIN + "-" + uin;
    };

    $rootScope.createUID  = function(uid){
        return $rootScope.chatAliases.UID + "-" + uid;
    };

    $rootScope.SetPrivate = function(UIN){ // PRIVATE
        CurrentDialog = {
            id: UIN, type: $rootScope.chatAliases.UIN, alias: mcConst.dataModels.PrivateInfo
        };
    };

    $rootScope.SetKanban  = function(){ // KANBAN
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.Kanban, alias: mcConst.dataModels.Kanban
        };
    };

    $rootScope.SetForum  = function(){
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.Forum, alias: mcConst.dataModels.Forum
        };
    };

    $rootScope.SetAdmin  = function(){
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.Admin, alias: mcConst.dataModels.Admin
        };
    };

    $rootScope.SetSendBroadcast = function(){ //
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.SendBroadcast, alias: mcConst.dataModels.SendBroadcast
        };
    };

    $rootScope.SetHistoryBroadcast = function(){ //
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.HistoryBroadcast, alias: mcConst.dataModels.HistoryBroadcast
        };
    };

    $rootScope.SetBBS     = function(){ // BBS
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.BBS, alias: mcConst.dataModels.BBS
        };
    };

    $rootScope.SetConf    = function(UID){ // CHANNEL
        CurrentDialog = {
            id: UID, type: $rootScope.chatAliases.UID, alias: mcConst.dataModels.ConfUserList
        };
    };

    // ====================================================

    $rootScope.checkAbilityByRights = function (where) {
        if (where){
            if (rightCheck[where]) {
                rightCheck[where]()
            } else {
                console.error("Was't set rights check for the \"" + where + "\"");
            }
        } else {
            for (var item in rightCheck){
                rightCheck[item]();
            }
        }
    };

    $rootScope.registerRightsCheck  = function (where, cb) {
        rightCheck[where] = cb;
    };

    $rootScope.startUrlOptions   = function () {
        var uin = $rootScope.Purl.param('uin');

        if (uin){
            $rootScope.$broadcast('OpenPrivate', [uin]);
        }
    };

    $rootScope.initSocketIo      = function () {
        if (window.hasOwnProperty("io") && mcService.isFunction(window.io) && !mcConst.socketIO){
            mcConst.socketIO = io({
                transports: mcConst.io_transport,
                'reconnection': true,
                'reconnectionAttempts': 5,
                'timeout' : 20000
            });

            mcConst.initWS(mcConst.socketIO);
        }
    };

    $rootScope.destroySocketIo   = function () {
        if (mcConst.socketIO){
            mcConst.socketIO.disconnect();
            mcConst.socketIO = null;
        }
    };

    $rootScope.spellCheckManager = null;

    $rootScope.checkNewVersion   = function(){
        if ($rootScope.restoreConnection) {
            $http("https://nsoft-s.com/files/mclastver.json").success(function(json){
                var verData = mcService.StringToObj(json);

                if (verData && verData.version && verData.version === mcConst.ClientSettingsData.ClientVersion){

                }
            }).catch(function (status) {

            });
        }
    };

    // ====================================================

    window._userActions = {};
    window._broadcastActions = {};
    window.moment       = moment || {};
    window.isMobile     = browserDetect.mobile;

    // ====================================================

    webix.Date.startOnMonday = true;

    mcService.clearPlaneObj($rootScope.mcStorage);
    mcConst.LoginInfo = mcService.LoadSessionSettings();

    // ====================================================

    var _msg = window._messages_.main = {
        SendCMDToServer : 'SendCMDToServer',
        ProcessCMD      : 'ProcessCMD',
        StartPingTimer  : 'StartPingTimer',
        StopPingTimer   : 'StopPingTimer',
        ReconnectLoop   : 'ReconnectLoop',
        ChangLanguage   : 'ChangLanguage',
        ErrorMsg        : 'ErrorMsg',
        hideAllErrors   : 'hideAllErrors',
        windowResize    : 'windowResize',
        confirmExit     : 'confirmExit'
    };

    $scope.$on(_msg.confirmExit, function (e, args) {
        var cb = args[0];

        if (cb){
            $rootScope.exitConfirmWindow = webix.confirm({
                type  : "confirm-error",
                text  : mcLang(603), // "603":"Выйти из чата?",
                ok    : mcLang(43),  // "43" :"Да",
                cancel: mcLang(44),  // "44" :"Нет",
                callback:function(yes){
                    if (yes){
                        cb();
                    }

                    // webix.modalbox.hide($rootScope.exitConfirmWindow);

                    $rootScope.exitConfirmWindow = null;
                }
            });
        }
    });

    $scope.$on(_msg.SendCMDToServer, function(e, args){
        sendCMD(args);
    });

    $scope.$on(_msg.ProcessCMD, function(e, args){
        receiveCMD(args);
    });

    $scope.$on(_msg.StartPingTimer, startPingTimer);

    $scope.$on(_msg.StopPingTimer, function(){
        if (mcConst.PingTimer) {
            if (mcConst.LoggedIn && mcStatistics && mcStatistics.saveStat){
                mcStatistics.saveStat(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN);
            }

            StopPingTimer();

            mcConst.PingTimer = null;
        }
    });

    $scope.$on(_msg.ReconnectLoop, function () {
        if (!mcConst.PingTimer && !$rootScope.restoreConnection && $rootScope.loginFormNotShowed && !$rootScope.systemSuspened) {
            $rootScope.restoreConnection = true;

            if (!reconnectTimer){
                reconnectTimer = setTimeout(function () {
                    reconnectTimer = null;

                    console.log("Reconnect!");

                    $rootScope.$broadcast(window._messages_.confUsers.clearUserForUID);
                    $rootScope.$broadcast(window._messages_.dialogsList.clearConfOnReconnect);

                    $rootScope.$broadcast('SendCMDToServer' , [
                        mcConst.lockInterface,
                        mcConst._CMD_.Login,

                        mcConst.LoginInfo.login,
                        mcConst.LoginInfo.pwd,
                        mcConst.LoginInfo.servPwd,
                        mcConst.LoginInfo.rm
                    ]);
                }, !$rootScope.isWebClient && mcConst.ClientSettings.NetworkReconnectTime ? mcConst.ClientSettings.NetworkReconnectTime * 1000 : mcConst.reconnectTime);
            }
        }
    });

    $scope.$on(_msg.ChangLanguage, function(e, args){
        (function(locale, lang){
            setLocalizations();

            lang = $location.param('lang') || lang;

            if (lang.toLowerCase() === 'ua') {
                lang = 'uk';
            }

            moment.locale(lang);

            mcConst.Lang = lang;

            webix.Date.startOnMonday = mcConst.Lang !== 'en';

            webix.skin.set('material');
            webix.skin.material.optionHeight = 30;

            // if ($rootScope.isWebClient) {
            //     $http(MC_RESOURCE.TextSource.smileys + 'smileys.json').success(function(txt){
            //         mcConst.SmileysSet = mcService.isObject(txt) ? txt : JSON.parse(txt);
            //     });
            // } else {
            //     var path = require('../service/profile').getProfilePath() + "/smileys/";
            //
            //     MC_RESOURCE.TextSource.smileys = path;
            //
            //     try {
            //         mcConst.SmileysSet = JSON.parse(require('fs').readFileSync(path + 'smileys.json'));
            //     } catch (e) {
            //         console.warn(e);
            //     }
            // }

            $http(MC_RESOURCE.TextSource.languages + lang + '.languages.json').success(function(txt){
                mcConst.LanguagesList = mcService.isObject(txt) ? txt : JSON.parse(txt);
            });

            $http(MC_RESOURCE.TextSource.errors.client + lang + '.errors.json').success(function(txt){
                mcConst.ErrorText = mcService.isObject(txt) ? txt : JSON.parse(txt);
            });

            $http(MC_RESOURCE.TextSource.www.admin + lang + '.json').success(function(txt){
                mcConst.LMSG_admin = mcService.isObject(txt) ? txt : JSON.parse(txt);
            });

            $http(MC_RESOURCE.TextSource.www.chat + lang + '.json').success(function(txt){
                mcConst.LMSG = mcService.isObject(txt) ? txt : JSON.parse(txt);

                if ($rootScope.isWebClient){
                    $scope.$broadcast('show' + mcConst.dataModels.Login, [lang]);
                } else {
                    $scope.prepareActions(lang);
                }

                mcService.CheckNotification();

                webix.i18n.setLocale(lang);
            });

            if (window.McSpellChecker){
                $rootScope.spellCheckManager = new McSpellChecker();
            }
        }).apply(null, args)
    });

    $scope.$on(_msg.ErrorMsg, function(e, args){
        if (errorMessagesIsShowed){
            if (errorMessages.length && errorMessages[errorMessages.length - 1][0] !== args[0]){
                errorMessages.push(args);
            }
        } else {
            displayError.apply(null, args);
        }
    });

    $scope.$on(_msg.hideAllErrors, function(){
        errorMessagesIsShowed = true;

        if (errorDialog) {
            webix.modalbox.hide(errorDialog);
        }

        errorDialog = null;

        errorMessagesIsShowed = false;
    });

    $scope.$on(_msg.windowResize, function(e, args){
        if (mcService.isFunction(args[0])){
            resizeWndCb.push(args[0]);
        }
    });

    // ====================================================

    $scope.$on(mcConst.lockInterface, function(e, args){
        if (!lockWindow || (lockWindow && !lockWindow.isVisible()) || lockCount === 0){
            lockTimer = setTimeout(function(){
                initLockWindow.apply(null, args);

                lockWindow.show();

                mcService.getElementByClass('webix_modal')[0].className += ' noLockScreen';
            }, 100);
        }

        lockCount ++;
    });

    $scope.$on('hide' + mcConst.lockInterface, function(e, args){
        if (args[0] === 'all'){
            lockCount = 0;
        }

        if (lockCount > 0) {
            lockCount --;
        }

        if (lockCount === 0 || !mcConst.PingTimer) {
            clearTimeout(lockTimer);

            lockTimer = null;

            lockWindow && lockWindow.hide();
        }
    });

    // === Webix Config ===========================================
         
    webix.rules.isNotEmpty    = function(val){
        return mcService.trim(val) !== '';
    };

    webix.rules.registerLogin = function(val){
        var res = false;
        var str = mcService.trim(val);

        if (str !== ""){
            for (var i = 0; i < str.length; i++){
                if ($rootScope.restrictedSymbols.indexOf(str[i]) !== -1){
                    res = false;
                    
                    break;
                } else {
                    res = true;
                }
            }
        }

        return res;
    };

    webix.rules.isNumber      = function(val){
        return /^\d+$/ig.test(val); // && !isNaN(parseInt(mcService.trim(val)));
    };

    webix.protoUI({
        name:"activeList"
    }, webix.ui.list, webix.ActiveContent);

    webix.protoUI({
        name:"moveTemplate"
    }, webix.ui.view, webix.Movable, webix.EventSystem);

    webix.protoUI({
        name:"suggest",
        defaults:{
            filter:function(item,value){
                return item.value.toString().toLowerCase().indexOf(value.toLowerCase()) !== -1;
            }
        }
    }, webix.ui.suggest);

    webix.ready(function(){
        // $scope.$emit(_msg.ChangLanguage, mcService.detectLang(webix.storage.local.get(mcConst.storageFields.lang)));
        $scope.$emit(_msg.ChangLanguage, mcService.detectLang(mcService.storage.get(mcConst.storageFields.lang)));
    });

    // ========================================================

    // window.onbeforeunload = function() {
    //     clearTimeout(reconnectTimer);
    //     reconnectTimer = null;
    //
    //     if (!$rootScope.isWebClient){
    //         sendCMD([
    //             mcConst._CMD_.ce_stop_ftp_server
    //         ], true);
    //
    //         mcConst.LoggedIn && sendCMD([
    //             mcConst._CMD_.cs_quit,
    //             mcConst.SessionID
    //         ]);
    //     } else
    //
    //     if (mcConst.LoggedIn){
    //         return mcService.Lang(40); // "40" : "Вы хотите завершить сеанс?",
    //     }
    // };

    window.onunload = function(){
        if (mcConst.LoggedIn) {
            !$rootScope.isWebClient && sendCMD([
                mcConst._CMD_.ce_stop_ftp_server
            ], true);

            $rootScope.isWebClient && sendCMD([
                mcConst._CMD_.cs_quit,
                mcConst.SessionID
            ]);
        }
    };

    mcService.addEvent(window, "resize", setWindowSize);
}

"use strict";

if (!window.old_IE) {
    var units   = []
/*
        .concat({ module: 'mcPlaySound',     args: ['$rootScope']})
        .concat({ module: 'mcConnect',       args: []})
        .concat({ module: 'mcEmojiList',     args: ['$rootScope']})
        .concat({ module: 'mcCmdLoop',       args: ['mcConnect',  '$rootScope']})
        .concat({ module: 'mcCmdParser',     args: ['mcConnect',  '$rootScope', 'mcPlaySound']})
        .concat({ module: 'CallStates',      args: []})
        .concat({ module: 'MediaCall',       args: []})
        .concat({ module: 'mcMedia',         args: ['mcPlaySound','$rootScope', 'MediaCall', 'CallStates']})
        .concat({ module: 'mcFileUploading', args: ['$rootScope']})
        .concat({ module: 'mcStorage',       args: ['$rootScope']})
        // .concat({ module: 'mcCall',          args: ['$rootScope', 'mcMedia']})
        .concat({ module: 'McMessageRender', args: ['$scope', '$rootScope', 'mcPlaySound', 'mcMedia', 'mcEmojiList']})

        .concat({ module: "mainController",  args: ['$scope', '$rootScope', 'mcCmdParser', 'mcCmdLoop', '$http',
                                                    '$location']})

        .concat({ module: 'loginController',            args: ['$scope', '$rootScope', '$location']})
        // .concat({ module: 'chatFrameController',        args: ['$scope', '$rootScope', 'mcPlaySound', 'mcCall']})
        .concat({ module: 'chatFrameController',        args: ['$scope', '$rootScope', 'mcPlaySound', 'McMessageRender', 'mcCall', 'mcEmojiList']})
*/
        .concat({ module: 'mcEmojiList',      args: ['$rootScope']})
        .concat({ module: 'mcPlaySound',      args: ['$rootScope']})
        .concat({ module: 'mcConnect',        args: []})
        // .concat({ module: 'mcCmdLoop',        args: ['mcConnect',  '$rootScope']})
        .concat({ module: 'CallStates',       args: []})
        .concat({ module: 'MediaCall',        args: []})
        // .concat({ module: 'mcMedia',          args: ['mcPlaySound','$rootScope', 'MediaCall', 'CallStates']})
        .concat({ module: 'mcDialogsList',    args: ['$rootScope', 'mcPlaySound']})
        .concat({ module: 'mcFileUploading',  args: ['$rootScope']})
        .concat({ module: 'mcStorage',        args: ['$rootScope']})
        .concat({ module: 'mcUploadProgress', args: ['$rootScope']})
        .concat({ module: 'mcCmdParser',      args: ['mcConnect',  '$rootScope', 'mcPlaySound', 'mcStatistics']})
        .concat({ module: 'McMessageRender',  args: ['$scope', '$rootScope', 'mcPlaySound', /*'mcMedia',*/ 'mcEmojiList']})
        .concat({ module: 'mcCallManager',    args: ['$scope', '$rootScope', 'mcPlaySound'/*, 'mcMedia'*/]})
        .concat({ module: 'mcUsersSmallInfo', args: ['$rootScope']})
        .concat({ module: 'mcAvatars',        args: ['$rootScope']})

        // .concat({ module: 'mcVoiceRecorder',  args: ['$rootScope']})

        .concat({ module: "mainController",   args: ['$scope', '$rootScope', 'mcCmdParser', /*'mcCmdLoop',*/ '$http',
                                                     '$location', 'mcStatistics', 'mcConnect']})

        .concat({ module: 'loginController',       args: ['$scope', '$rootScope', '$location']})
        .concat({ module: 'chatFrameController',   args: ['$scope', '$rootScope', 'mcPlaySound', 'McMessageRender', /*'mcCallManager',*/ 'mcVoiceRecorder', 'mcEmojiList']})
    ;

    window.mcService = servicesController('support');
    window.mcLang    = mcService.Lang;

    mcComponents.units(units).start();
}

