;(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,
    ElisaName: "Elisa",

    mediaConf: {
        NONE : 0,
        VOICE: 1,
        VIDEO: 2,
        WEBINAR: 3
    },

    mediaType: {
        mic: "microphone",
        cam: "camera"
    },

    isWindowFocused: true,

    io_transport: ["polling"], // , "websocket"

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

    socketIO : null,
    
    // ClientSettings: {},
    avatars: {
        SMALL : 19,
        MEDIUM: 23,
        LARGE : 32
    },

    traffic_type : {
        all: 1,
        cmd: 2,
        off: 3
    },

    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-',
        COMMONTREE      : 'common_tree-',
        PERSONALTREE    : 'personal_tree-',
        POPULARSMILES   : 'popular_smiles-',

        // -------

        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',
    googleMapsContainer: 'googleMapsContainer',

    storageFields: {
        lang    : 'cliLang',
        Login   : "cliLogin",
        Pwd     : "cliPwd",
        ServPwd : "cliServPwd",
        Rm      : "cliRm",
        AutoRld : "cliNoAutoConnect"
    },

    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',
        Terminal         : 'Terminal',

        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   : {                     // информация о залогиненом юзере
        MaxSelfMsgEditTime: 15,
        MaxUploadFileSize : 0,
        OptionsPresetCRC32: 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:""},
        DisplayName     : "__Anonymous__",
        onlineState     : -1,
        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,
        GeneralContactsSortStyle: 1,

        // "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 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();
     };
 }

 /**
 * 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;
 }

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;
        }
    };
}
// created 21.09.2022 by Gifer

function McAudioConf($rootScope){
    "use strict";

    const commands  = {
        sendOffer      : '0001',
        sendAnswer     : '0002',
        sendICE        : '0003',
        receiveOffer   : '0004',
        receiveAnswer  : '0005',
        receiveICE     : '0006',

        connect        : '0007',
        disconnect     : '0008',
        setID          : '0009',
        newClient      : '0010',

        nm_ready       : '0011',
        nm_create_room : '0012',
        nm_remove_room : '0013',

        mn_create_room : '0014',
        mn_remove_room : '0015',
        mn_ready       : '0016',
    };
    const MIXER     = "MIXER";

    const broadcast = {
        speech_start     : "0001",
        speech_stop      : "0002",
        mic_on           : "0003",
        mic_off          : "0004",
    }

    var MID         = null;
    var isMyConf    = false;
    var localStream = null;
    var myClientID  = null;
    var _config     = {};
    var peerConn    = null;

    function initTrack(track, callID){
        if (track.kind === "audio"){
            var audioStreams   = new MediaStream();
            var audioContainer = document.createElement("audio");
            audioContainer.srcObject = audioStreams;

            audioStreams.addTrack(track);

            audioContainer.play();

            // window.AudioContext = window.AudioContext || window.webkitAudioContext;
            // var audioContext = new AudioContext();
            // var source = audioContext.createMediaStreamSource(audioStreams);

            // Setup options
            // var options = {
            //     source: source,
            //     voice_stop: function() {console.log('voice_stop');},
            //     voice_start: function() {console.log('voice_start');}
            // };

            // Create VAD
            // var vad = new VAD(options);
        }

        // callID && saveTrackID(callID, track.id);
    }

    function getLocalStream() {
        console.log('Requesting local stream');

        return navigator.mediaDevices.getUserMedia({
            audio: true,
            video: false
        }).then(function (stream){
            console.log('Received local stream');

            localStream = stream;

            // initTrack(localStream.getAudioTracks()[0]);
        });
    }

    function call(incomingOffer) {
        if (!peerConn) {
            peerConn = new RTCPeerConnection({
                iceTransportPolicy: _config.iceTransportPolicy, //"relay",
                iceTransport      : _config.iceTransportPolicy, //"relay",
                iceServers        : _config.iceServers,
                sdpSemantics      :'unified-plan'
            });

            // peerConn.clientID = callID;

            console.log('Created local peer connection');

            peerConn.addEventListener('icecandidate', function (e){
                if (e.candidate) {
                    __SendData(commands.sendICE, {
                        ice: e.candidate,
                        uin: mcConst.UserInfo.UIN
                    });
                }
            });

            peerConn.addEventListener('iceconnectionstatechange', function (evt) {
                console.log("ICE connection state change: " + evt.target.iceConnectionState);

                if (peerConn.iceConnectionState === 'disconnected') {

                } else
                if (peerConn.iceConnectionState === 'connected' || peerConn.iceConnectionState === 'completed') {


                } else
                if (peerConn.iceConnectionState === 'failed' || peerConn.iceConnectionState === 'closed') {
                    // removeStreamUser(callID);

                    peerConn = null;
                }
            });

            peerConn.onerror = function(err) {
                console.log(err);
            };

            peerConn.ontrack = function (str){
                console.log("Get " + str.track.kind + " track id: " + str.track.id);
                console.log("Get " + str.track.kind + " track label: " + str.track.label);

                initTrack(str.track/*, callID*/);
            };

            peerConn.onsignalingstatechange = function (ev) {
                console.log(ev.type + ": " + peerConn.signalingState);
            };

            peerConn.icecandidateerror = function (event){
                console.log("peerConnection.icecandidateerror: " + event.errorCode);
            };

            // peerConn.onnegotiationneeded = function (event){
            //     console.log("peerConnection.onnegotiationneeded");
            // };

            console.log('Added local stream');

            peerConn.addStream(localStream);
        }

        // ====================================================
        // === Send Offer | Answer ============================
        // ====================================================
        if (incomingOffer){
            console.log('Create Answer');

            peerConn.setRemoteDescription(new RTCSessionDescription(incomingOffer));

            peerConn.createAnswer().then(function (outgoingAnswer){
                peerConn.setLocalDescription(outgoingAnswer);

                console.log('setLocalDescription complete and Send Answer');

                __SendData(commands.sendAnswer, {
                    answer: outgoingAnswer,
                    uin   : mcConst.UserInfo.UIN
                });
            }).catch(function (err){
                console.log(err.message);
            });
        } else {
            console.log('Create Offer');

            peerConn.createOffer({
                voiceActivityDetection: 1,
                iceRestart            : 1,
                offerToReceiveAudio   : 1
            }).then(function (outgoingOffer){
                peerConn.setLocalDescription(outgoingOffer);

                console.log('setLocalDescription complete and Send Offer');

                __SendData(commands.sendOffer, {
                    offer: outgoingOffer,
                    uin  : mcConst.UserInfo.UIN
                });
            }).catch(function (err){
                console.log(err.message);
            });
        }
    }

    function onReceiveOffer({offer}) {
        call(/*id,*/ offer);
    }

    function onReceiveAnswer({answer}) {
        // pcList[id] && pcList[id].setRemoteDescription(new RTCSessionDescription(answer));
        peerConn && peerConn.setRemoteDescription(new RTCSessionDescription(answer));
    }

    function onReceiveICE({ice}) {
        // pcList[id].addIceCandidate(new RTCIceCandidate(ice));
        peerConn && peerConn.addIceCandidate(new RTCIceCandidate(ice));
    }

    function __SendData(cmd, args) {
        $rootScope.$broadcast("SendCMDToServer", [
            mcConst._CMD_.cs_media_conf,
            mcConst.SessionID,
            
            JSON.stringify({
                UID : _config.conf_uid,
                cmd : cmd,
                Data: args
            })
        ]);
    }

    function start(mid) {
        myClientID = _config.SelfUIN;

        MID = mid;

        getLocalStream().then(function (){
            call();
        }).catch(function (err){
            console.log(err.message);
        });
    }

    // ===== Client Start =====

    $rootScope.$on("sc_media_conf", function (e, args){
        var info = args[0];

        console.log('sc_media_conf: ' + info.cmd);

        switch (info.cmd){
            case commands.receiveOffer:
                onReceiveOffer(info.Data);
            break;

            case commands.receiveAnswer:
                onReceiveAnswer(info.Data);
            break;

            case commands.receiveICE:
                onReceiveICE(info.Data);
            break;

            case commands.disconnect:
                console.log("--- Client disconnected: " + info.Data);

                // removeStreamUser(info.data);
            break;
        }
    });

    this.joinToConf = function (uid, cb){
        if (!_config.conf_uid) {
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_media_conf_join,
                mcConst.SessionID,

                uid,

                function (info) {
                    _config = {
                        iceTransportPolicy: $rootScope.isWebClient ? "relay" : mcConst.ClientSettings.IceTransportPolicy,
                        conf_uid          : uid,
                        SelfUIN           : mcConst.UserInfo.UIN,
                        iceServers        : mcService.makeIceServers({
                            IPList    : info.IPList,
                            TURNPort  : info.TURNPort,
                            UserName  : info.UserName,
                            Credential: info.Credential
                        }, $rootScope.isWebClient)
                    }

                    start(_config.conf_uid);

                    cb && cb(info);
                }
            ]);
        }
    };

    this.isLive = function (){
        return !!peerConn;
    };

    this.getMID = function (){
        return MID;
    };

    this.setMID = function (mid, myConf) {
        MID      = mid;
        isMyConf = !!myConf;
    }

    this.closeConf = function (mid) {
        if (mid == MID && isMyConf){
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_media_conf_close,
                mcConst.SessionID,

                MID
            ]);
        }
    }

    this.closeCall = function (mid){
        if (mid == MID){
            localStream && localStream.getTracks().forEach(function(track){
                track.stop();
            });

            peerConn && peerConn.close();
            peerConn    = null;
            MID         = null;
            myClientID  = null;
            localStream = null;
            isMyConf    = false;
            _config     = {};
        }
    };
    
    this.leaveConf = function (cb){
        if (MID) {
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_media_conf_leave,
                mcConst.SessionID,

                MID,

                function () {
                    localStream && localStream.getTracks().forEach(function(track){
                        track.stop();
                    });

                    peerConn && peerConn.close();
                    peerConn    = null;
                    myClientID  = null;
                    localStream = null;
                    _config     = {};

                    if (!isMyConf) {
                        MID = null;
                    }

                    cb && cb();
                }
            ])
        }
    };
}
/*
 * 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);
    }
}

function mcCmdLoop (mcConnect, $rootScope){
    "use strict";

    window.idMcPingTimer = null;

    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]]);
            }
        }
    }

    function cmdPing (sID){
        $rootScope.SendCMDToServer([mcConst._CMD_.Ping, sID]);
    }



    return function StartPingTimer(_time, sID, opt){ // this.StartPingTimer =
        var self = {};
        var options =  opt || {
            onError : null,
            onStop  : null,
            onWarn  : null
        };

        mcConnect.ClearAllCallBackFunctionByCMD();
        mcConnect.LeaveCallBacks = true;

        mcConnect.SetCallBackFunctionByCMD(mcConst._CMD_.Ping, function(){
            var data = this;

            if ((data !== "{}") && (data.length > 0)) {
                RunCMD(data);
            }
        });

        if (mcConst.SessionID)
            window.idMcPingTimer = setInterval(function(){
                if (mcConst.SessionID) {
                    cmdPing(mcConst.SessionID);
                } else {
                    console.error("mcConst.SessionID is not defined. StopPingTimer()");

                    self.StopPingTimer();
                }
            }, _time);

        self.StopPingTimer = function(){
            if (window.idMcPingTimer !== null){
                clearInterval(window.idMcPingTimer);

                window.idMcPingTimer = null;

                mcConnect.ClearAllCallBackFunctionByCMD();

                if (options.onStop) {
                    options.onStop();
                }
            }
        };

        mcConnect.OnError = function(err){
            if (console) {
                console.warn('An error occurred: Ping-Timer has been Stopped');
            }

            self.StopPingTimer();

            if (options.onError){
                options.onError(err);
            }
        };

        return self;
    }
}

/**
 * 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;
}

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 () {
     "use strict";

     if (window.mcWidgets){
         window.mcWidgets.ButtonsListWithContext = function ButtonsListWithContext($rootScope){
             function show(btn, options, list, fillParams){
                 var wndId = mcService.RandomHash(7);

                 if (mcService.isString(list)) {
                     list = $$(list);
                 }

                 $rootScope.hotKeyDispatcher.addPreset(wndId, [{
                     key : mcConst.keyCodes.enter,
                     lockPrev: true,
                     func: function(){
                         var form = btn.dialog.getChildViews()[1];

                         if (btn.confirm.apply(form, [].concat(form.getChildViews()).concat(list ? list.getSelectedItem() : [])) !== true) {
                             // если возвращает true, то зыкрывать диалоговое окно не нужно, в поле для ввода текста есть ошибка и т.д.
                             btn.dialog.close();
                         }
                     }
                 }, {
                     key : mcConst.keyCodes.esc,
                     lockPrev: true,
                     func: function(){
                         btn.dialog.close();
                     }
                 }], document, true);

                 btn.dialog = webix.ui(mcService.Marge(options, {
                     view     : "window",
                     position : "center",
                     id       : wndId,
                     head     : btn.title,
                     body     : {
                         view : 'form',
                         rules:{
                             "Name"    : webix.rules.isNotEmpty
                         },
                         elements : [{
                             view       : "text",
                             label      : btn.editTitle,
                             name       : 'Name',
                             labelPosition : 'top',
                             required   : "true"
                         }]
                         .concat(btn.additionalItems ? btn.additionalItems : [])
                         .concat({
                             cols : [{
                                 view  : "button",
                                 value : btn.okTitle,
                                 click : function() {
                                     var form = this.getFormView();

                                     if (btn.confirm.apply(form, [].concat(form.getChildViews()).concat(list ? list.getSelectedItem() : [])) !== true) { // если возвращает true, то зыкрывать диалоговое окно не нужно, в поле для ввода текста есть ошибка и т.д.
                                         btn.dialog.close();
                                     }
                                 }
                             }, {
                                 view  : "button",
                                 type  : "danger",
                                 value : btn.cancelTitle,
                                 click : function(){
                                     btn.dialog.close();

                                     if (btn.abort){
                                         btn.abort();
                                     }
                                 }
                             }]
                         })
                     },
                     on : {
                         onShow : function(){
                             if (fillParams) {
                                 this.getChildViews()[1].getChildViews().forEach(function(item, id){
                                     if (btn.fillItems[id] != undefined) item.setValue(list.getSelectedItem()[btn.fillItems[id]]);
                                 });
                             }

                             this.getChildViews()[1].getChildViews()[0].focus();

                             // $rootScope.$broadcast('addShowedWindow', [wndId, this]); // сообщаем менеджеру переходов между экранами, что появилось новое окно и его нужно закрыть при изменении экрана
                         },
                         onDestruct : function(){
                             $rootScope.hotKeyDispatcher.removePreset(wndId);
                         }
                     }
                 }));

                 if (!btn.onBeforeShow || (btn.onBeforeShow && btn.onBeforeShow.apply(btn, [list.getSelectedItem(), list]))){
                     btn.dialog.show();
                 } else {
                     btn.dialog.close();
                 }
             }

             var self = this;
             var params = [];

             this.editButtons = function(_params){
                 var ctx        = null;
                 var resButtons = {};

                 params = mcService.Marge({
                     buttons   : [{
                         additionalItems: [],
                         type           : 'add', // add, edit, del
                         confirm        : null,
                         abort          : null,
                         addToContext   : false,
                         showedInList   : true,
                         css            : '',
                         contextCss     : '',
                         label          : '',
                         icon           : '',
                         contextIcon    : '',
                         title          : '',
                         editTitle      : '',
                         fillItems      : [],
                         delName        : '',
                         onBeforeShow   : null,
                         onBeforeDelete : null
                     }],

                     fillItems : [],
                     list      : null,
                     vertical  : false,
                     onHideContext: null,
                     onShowContext: null,

                     options   : {
                         modal : true,
                         head  : '',
                         width : 300
                     },
                     contextOpt: {
                         width: 200
                     }
                 }, _params);

                 var btnsList  = [];
                 var direction = params.vertical ? "rows" : "cols";

                 params.buttons.forEach(function (btn) {
                     var _btn = {};

                     function closeContext() {
                         if (btn.addToContext && self.contextMenu){
                             if (self.contextMenu) {
                                 self.contextMenu.hide();
                             }
                         }
                     }

                     switch (btn.type){
                         case 'add':
                             _btn = { view: "button", click: function(){ // "50" : "Добавить",
                                 closeContext();

                                 show(btn, params.options, params.list);
                             }};
                         break;

                         case 'edit':
                             _btn = { view: "button", click: function(){ // "51" : "Изменить",
                                 closeContext();

                                 show(btn, params.options, params.list, true);
                             }};
                         break;

                         case 'del':
                             _btn = { view: "button", type: "danger", click: function(){ // "52" : "Удалить",
                                 closeContext();

                                 if (mcService.isString(params.list)) {
                                     params.list = $$(params.list);
                                 }

                                 var item = params.list.getSelectedItem();

                                 if (item && (!btn.onBeforeDelete || (btn.onBeforeDelete && btn.onBeforeDelete.apply(btn, [item, params.list])))){
                                     $rootScope.$broadcast('addShowedWindow', [mcService.RandomHash(4), webix.confirm({
                                         type  : "confirm-warning",
                                         text  : mcService.myReplace(btn.title, btn.delName ? item[btn.delName] : null), // "902":"Удалить должность \"%s\"?",
                                         ok    : btn.delTitle,
                                         cancel: btn.cancelTitle,
                                         callback:function(yes){
                                             if (yes){
                                                 btn.confirm(item);
                                             }
                                         }
                                     })]);
                                 }
                             }};
                         break;
                     }

                     if (!btn.fillItems || !btn.fillItems.length){
                         btn.fillItems = params.fillItems;
                     }

                     _btn._opt = btn;

                     btnsList.push(_btn);
                 });

                 resButtons[direction] = [];

                 btnsList.forEach(function (__btn) {
                     var btn = mcService.Marge({}, __btn);

                     if (btn._opt.confirm && btn._opt.showedInList){
                         if (btn._opt.icon){
                             btn.type  = 'icon';
                             btn.icon  = btn._opt.icon;
                             btn.label = btn._opt.label;
                         } else {
                             btn.value = btn._opt.label;
                         }

                         btn.css  = btn._opt.css || undefined;

                         resButtons[direction].push(mcService.Marge({}, btn));
                     }
                 });

                 function attachContext() {
                     if (!self.contextMenu){
                         var contextButtons = [];

                         btnsList.forEach(function (__btn) {
                             var btn = mcService.Marge({}, __btn);

                             if (btn._opt.confirm && btn._opt.addToContext){
                                 if (btn._opt.contextIcon){
                                     btn.type  = 'icon';
                                     btn.icon  = btn._opt.contextIcon;
                                     btn.label = btn._opt.label;
                                 } else {
                                     btn.value = btn._opt.label;
                                 }

                                 if (params.contextOpt.btnHeight){
                                     btn.height = params.contextOpt.btnHeight;
                                 }

                                 btn.css  = btn._opt.contextCss || undefined;
                                 btn._opt = mcService.Marge({
                                     okTitle        : mcLang(28), // "28" :"Сохранить",
                                     cancelTitle    : mcLang(29), // "29" :"Отмена",
                                     delTitle       : mcLang(581) // "581": "Удалить",
                                 }, btn._opt);

                                 contextButtons.push(mcService.Marge({}, btn));
                             }
                         });

                         ctx = mcService.Marge(params.contextOpt, {
                             view  : "context",
                             master: mcService.isString(params.list) ? $$(params.list) : params.list,
                             body  : { rows: contextButtons },
                             on    : {}
                         });

                         if (params.onShowContext){
                             ctx.on.onShow = params.onShowContext;
                         }

                         if (params.onHideContext){
                             ctx.on.onHide = params.onHideContext;
                         }

                         if (contextButtons && contextButtons.length){
                             self.contextMenu = webix.ui(ctx);
                         }
                     }
                 }

                 if (!resButtons[direction].length) {
                     resButtons.width  = 1;
                     resButtons.height = 1;
                 }

                 return {
                     btnList   : resButtons,
                     emitAction: function(idx, edit){
                         var isString = mcService.isString(idx) && isNaN(parseInt(idx));

                         if (isString){
                             idx = mcService.findItemInArrayOfObj(params.buttons, idx, 'name');
                         }

                         if (params.buttons[idx]){
                             show(params.buttons[idx], params.options, edit);
                         }
                     },
                     hideContext : function () {
                         if (self.contextMenu) {
                             self.contextMenu.hide();
                         }
                     },
                     attachContext: attachContext,
                     destroy      : function () {
                         if (ctx){
                             ctx.destructor();
                         }
                     }
                 };
             };
         }

     } else {
         console.error('window.mcWidgets is not defined!');
     }
 })();

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();
}
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 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;
    var traffic_view    = mcConst.traffic_type.off;

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

    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);
        }
    });

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

    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 __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();
        }

        $rootScope.$broadcast(window._messages_.dialogsList.loadPrivateDialogsList, [function (){
            if ($rootScope.GetChatTypeID() === "UIN--1"){
                var uin = $rootScope.$broadcast(window._messages_.dialogsCtrl.getFirstDialog);

                $rootScope.$broadcast(window._messages_.dialogsList.OpenPrivate, [uin]);
            }
        }]);
        $rootScope.$broadcast(window._messages_.dialogsList.loadHistoryDialogsList, []);
        $rootScope.$broadcast(window._messages_.dialogsList.getIgnoreList, []);

        if (!$rootScope.isWebClient){
            $rootScope.$broadcast(window._messages_.clientData.enableGlobalHotKeys);
            $rootScope.$broadcast(window._messages_.main.hideAllErrors);
            $rootScope.$broadcast(window._messages_.clientData.setClientTitle);

            if ($rootScope.restoreConnection){
                $rootScope.restoreConnection = false;
                $rootScope.LoginIsFinished = true;

                $rootScope.$broadcast(window._messages_.chatWrapper.showHideMenu, false);

                setTimeout(function (){
                    $rootScope.$broadcast(window._messages_.chatFrame.restoreMessage);
                }, 100);

                return; //  внезапный выход, факиншыт !!!
            }

            if (mcConst.ServerInfo.ID && !$rootScope.acConfig.serversInfo[mcConst.ServerInfo.ID].ServerID){
                $rootScope.$broadcast(window._messages_.serversManager.addServerOnSuccessConnect, mcConst.ServerInfo.ID);
            } else {
                $rootScope.$broadcast(window._messages_.clientData.saveCurrentServerID, [$rootScope.acConfig.serversInfo[mcConst.ServerInfo.ID].ServerID]);
            }

            if (mcConst.LoginInfo.rm){
                $rootScope.UNIT.ctrlServersManager.setAutoConnectInfo(mcConst.LoginInfo.pwd, mcConst.LoginInfo.rm, mcConst.LoginInfo.servPwd);
            } else {
                $rootScope.UNIT.ctrlServersManager.setAutoConnectInfo("", false, "");
            }
        } else {
            $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;

        if (mcConst.UserInfo.hasInvite){
            if ($rootScope.GetChatType() === $rootScope.chatAliases.UIN && $rootScope.GetChatID() != -1){
                $rootScope.$broadcast('OpenPrivate', [$rootScope.GetChatID()]);
            }
        }
    }

    function __sendTraffic(inTraf, cmd, args){
        if (traffic_view !== mcConst.traffic_type.off)
        switch (traffic_view){
            case mcConst.traffic_type.cmd:          // in
                $rootScope.$broadcast("traffic_log", [inTraf, cmd]);
            break;

            case mcConst.traffic_type.all:
                $rootScope.$broadcast("traffic_log", [inTraf, ([cmd].concat(mcService.ObjToString(args))).join('')]);
            break;

            //default:
        }
    }

    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;

                    __sendTraffic(!send, cmd, args);

                    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: нет входящих аргументов"
        }
    }

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

    $rootScope.$on("traffic_view", function (e, args) {
        traffic_view = args[0];
    });

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

    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;
        }
    }

    // === Electron ======================

    function runElectronError(err){
        if ($rootScope.ElectronErrors[err.code]){
            $rootScope.ElectronErrors[err.code](err);
        } else {
            return false;
        }
    }

    this.internalSender = function (args) {   // client => electron "ce_"
        if (electronHandler){
            electronHandler.send('ce_CMD', args); // args = [cmd, ...]
        } else {
            throw new Error('Electron handler not initialized!')
        }
    };

    $rootScope.$on('prepareElectron', function () {
        if (window.require){
            var electron = require('electron');
            electronHandler = electron.ipcRenderer;

            // window.__Notification = window.Notification;
            // window.Notification   = require('@electron/remote/main').Notification;

            var webFrame = electron.webFrame;

            webFrame.setZoomFactor(1);
            webFrame.setVisualZoomLevelLimits(1, 1);
            // webFrame.setLayoutZoomLevelLimits(0, 0);

            electronHandler.on('ec_CMD', function (e, args) { // electron => client "ec_"
                var cmd  = args[0];
                var data = args[1];

                if (eCmd.hasOwnProperty(cmd)){
                    customCallBack(eCmd[cmd], data);
                } else
                if (ProcessCMD.hasOwnProperty(cmd)) {
                    ProcessCMD[cmd](data);
                } else {
                    console.warn(mcLang(9, ['ProcessCMD', cmd.toString()])); // "10" : "%s: %s not found!"
                }
            });

            electronHandler.on('sc_CMD', function (e, args) {
                var cmd = Object.keys(args)[0];

                $rootScope.ProcessCMD([cmd, args[cmd]]);
            });

            return electronHandler;
        }
    });

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

    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
            });

            __sendTraffic(false, cmd, 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));

            __sendTraffic(true, cmd, 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;

    // pCmd[_CMD_.sc_get_geo_position]               = _CMD_.cs_geo_position;
    pCmd[_CMD_.sc_user_gps_history]               = _CMD_.cs_get_user_gps_history;
    pCmd[_CMD_.sc_media_conf_join]                = _CMD_.cs_media_conf_join;
    pCmd[_CMD_.sc_media_conf_leave]               = _CMD_.cs_media_conf_leave;
    pCmd[_CMD_.sc_conf_user_list]                 = _CMD_.cs_get_conf_user_list;
    pCmd[_CMD_.sc_users_avatars]                  = _CMD_.cs_get_user_avatars;

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

    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.onlineState = _data.State;

        // console.log(`My online state: ${mcConst.UserInfo.State}`);
        
        mcConst.UserInfo.DisplayName       = mcConst.UserInfo.Nick;
        mcConst.UserInfo.MaxUploadFileSize = mcConst.UserInfo.MaxUploadFileSize * 1024 * 1024;
        mcConst.UserInfo.ServID            = mcConst.UserInfo.ServID || mcConst.UserInfo.ServerID;
        mcConst.UserInfo.TechID            = mcConst.UserInfo.TechID || 0;

        $rootScope.$broadcast(window._messages_.dialogsList.setPrivatDialogsIdx, [mcConst.UserInfo.PvDlgIdx]);

        $rootScope.$broadcast(window._messages_.usersSmallInfo.setAllUsersInfo, [[{
            UIN        : 0,
            DisplayName: mcConst.ElisaName,
            ChangeID   : 0,
            Avatar     : 0,
            Sex        : -1,
            TeamLead   : true
        }]]);

        if (!$rootScope.restoreConnection){
            mcConst.settingsCRC32 = mcConst.UserInfo.OptionsPresetCRC32;

            $rootScope.$broadcast('hide' + mcConst.dataModels.Login, []);
        } else {
            $rootScope.$broadcast('close' + mcConst.dataModels.UserProfile, []);
            $rootScope.$broadcast('close' + mcConst.dataModels.Settings, []);
        }

        if (mcConst.UserInfo.onlineState !== mcConst.states.dnd && (mcConst.UserInfo.onlineState === mcConst.states.away || mcConst.UserInfo.onlineState === mcConst.states.offline || mcConst.UserInfo.onlineState === mcConst.states.webOnline)){
            console.log('Send set state online');

            $rootScope.$broadcast('SendCMDToServer' , [
                mcConst._CMD_.cs_set_state,
                mcConst.SessionID,

                mcConst.states.online
            ]);

            mcConst.UserInfo.onlineState = mcConst.states.online;
        }
        
        $rootScope.$broadcast(window._messages_.usersSmallInfo.initDB, [mcConst.UserInfo.ServID, function (){
            var usersID  = $rootScope.$broadcast(window._messages_.usersSmallInfo.getMaxUserID);
            var nextStep = function (list) {
                if (list && list.length) {
                    $rootScope.$broadcast(window._messages_.usersSmallInfo.setAllUsersInfo, [mcService.StringToObj(list), true]);
                }

                usersID = $rootScope.$broadcast(window._messages_.usersSmallInfo.getMaxUserID);

                if (!mcConst.UserInfo.hasInvite) {
                    mcService.SaveSessionSettings(mcConst.LoginInfo);
                }

                $rootScope.$broadcast(window._messages_.dialogsList.loadUserStatuses, []);

                $rootScope.SendCMDToServer([
                    mcConst._CMD_.cs_login_done,
                    mcConst.SessionID
                ]);

                if (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.LoggedIn = true;
            }

            console.log("usersID: " + usersID + " maxID: " + mcConst.UserInfo.UsersID);

            $rootScope.$broadcast(window._messages_.avatarsManager.loadAllAvatars);

            if ((usersID === 0 || usersID < mcConst.UserInfo.UsersID) && !mcConst.UserInfo.hasInvite){
                $rootScope.$broadcast('SendCMDToServer', [
                    mcConst._CMD_.cs_get_fresh_users_data,
                    mcConst.SessionID,

                    usersID,

                    nextStep
                ])
            } else {
                nextStep();
            }
        }]);
    };

    ProcessCMD[_CMD_.sc_pong]                     = function(data) {
        var info = mcService.StringToObj(data);

        customCallBack(_CMD_.cs_ping2, info);
    };

    ProcessCMD[_CMD_.sc_all_users_changed]        = function(data) {
        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_get_fresh_users_data,
            mcConst.SessionID,

            0,

            function (list) {
                if (list && list.length) {
                    $rootScope.$broadcast(window._messages_.usersSmallInfo.setAllUsersInfo, [mcService.StringToObj(list), true]);
                }

                // usersID = $rootScope.$broadcast(window._messages_.usersSmallInfo.getMaxUserID);
            }
        ])
    };

    ProcessCMD[_CMD_.sc_login_finish]             = function(data) {
        // "CID"           : 3,        // пользователь успешно авторизовался, необязательный параметр
        // "Ok"            : true,     // успешное соединение, необязательный параметр
        // "RobotAvatar"   : 0,        // (8.0+) аватар встроенного робота
        // "RobotNick"     : "Elisa",  // (8.0+) ник встроенного робота
        // "AutoNotifiers" : true,     // (8.7+) показывать автоматические важные уведомления про функции чата
        // "UpdateGPS"     : 15,       // (8.12+) время в минутах, как часто отправлять на сервер свою позицию

        var info = mcService.StringToObj(data);
        var elisaInfo = $rootScope.$broadcast(window._messages_.usersSmallInfo.getAllUsersInfo, [0]);

        if (!elisaInfo || elisaInfo.DisplayName !== info.RobotNick){
            $rootScope.$broadcast(window._messages_.usersSmallInfo.setAllUsersInfo, [[{
                UIN        : 0,
                DisplayName: info.RobotNick,
                ChangeID   : 0,
                Avatar     : 0, //info.RobotAvatar,
                Sex        : -1,
                TeamLead   : true
            }]]);
        }

        console.log('--------========== LOGGED IN! ==========--------');

        mcConst.ElisaName = info.RobotNick;

        __loginFinish();

        if (!$rootScope.isWebClient){
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [mcConst._CMD_.ce_login_finish, mcConst.UserInfo.AutoAwayTime]);
        }

        $rootScope.checkNewVersion();

        $rootScope.$broadcast('hide' + mcConst.lockInterface, ["all"]); // unlock
    };

    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" : 224,        // номер ошибки
            "Params" : [          // параметры ошибки, если есть, параметров может быть любое количество, все - текстовые строки
                "bla-bla-bla",
                "test-test-test"
            ]
        };
        err.ErrNum = parseInt(err.ErrNum);

        if (!mcConst.LoggedIn){
            mcConst.UserInfo.PortNode = err.PortNode;
            mcConst.UserInfo.HTTPS    = err.HTTPS;
        }

        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

        //  "224": "сетевая ошибка: %s"

        if (err.ErrNum !== mcConst._CMD_.Errors.NetworkError ||
            (!$rootScope.loginFormNotShowed && (
                err.ErrNum === mcConst._CMD_.Errors.WebServerStopped ||
                err.ErrNum === mcConst._CMD_.Errors.NetworkError ||
                err.ErrNum === mcConst._CMD_.Errors.ServerBusy
            ))){
            $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);
            }]);
        } else
        if (err.ErrNum === mcConst._CMD_.Errors.NetworkError && $rootScope.isWebClient) {
            $rootScope.$broadcast('StopPingTimer', [true]); // is error

            // mcConst.LoggedIn = false;
            //
            // $rootScope.$broadcast('hide' + mcConst.dataModels.ChatWrapper, []);
            //
            // $rootScope.$broadcast('ErrorMsg', [err.ErrNum, mcLang(14), "", function(){ // "14" :"Потеряно соединение с сервером",
            //     location.search = "";
            //     location.reload();
            // }]);
        }
    };

    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.dropConnectOnLogin && $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);

                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_break_blink
                ]);
            }
        } 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]);

            $rootScope.$broadcast('sendCMDToElectron', [
                mcConst._CMD_.ce_break_blink
            ]);
        }
    };

    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_remove_conversations(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) {
            info.State = !!info.State;

            $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

            $rootScope.$broadcast(window._messages_.dialogsList.addNewConfData, [mcService.Marge({}, info)]);
            // $rootScope.$broadcast(window._messages_.dialogsList.addNewConfData, [structuredClone(info)]);

            var setUserInfo = function (info) {
                $rootScope.$broadcast(window._messages_.usersSmallInfo.getInfoByUserUINs, [info.UINS, function (users) {
                    $rootScope.$broadcast(window._messages_.dialogsList.confUserList, [info.UID, users, info.UINS]);

                    customCallBack(_CMD_.cs_join_txt_channel, info);
                }, mcConst.UserInfo.hasInvite]);
            };

            var getFreshInfo =  function () {
                $rootScope.$broadcast("SendCMDToServer", [
                    mcConst._CMD_.cs_get_conf_user_list,
                    mcConst.SessionID,
                    info.UID,

                    function (newInfo) {
                        $rootScope.$broadcast(window._messages_.usersSmallInfo.setConfUins, [newInfo.UID, newInfo.UserListCRC32, newInfo.UINS]);

                        setUserInfo(newInfo);
                    }
                ]);
            };

            $rootScope.$broadcast(window._messages_.usersSmallInfo.getConfUins, [info.UID, function (list) {
                if (list && list.length > 0) {
                    if (list[0].CRC32 === info.UserListCRC32) {
                        setUserInfo(list[0]);
                    } else {
                        getFreshInfo();
                    }
                } else {
                    getFreshInfo();
                }
            }]);

            customCallBack(_CMD_.cs_create_txt_channel, info);
        }
    };

    ProcessCMD[_CMD_.sc_user_gps_history]         = function sc_user_gps_history(data){
        var info = mcService.StringToObj(data);

        if (!customCallBack(_CMD_.sc_user_gps_history, info)) {
            $rootScope.$broadcast(window._messages_.chatFrame.on_geopositionHistory, [info]);
        }
    };

    ProcessCMD[_CMD_.sc_remove_users]             = function sc_remove_users(data){
        var info = mcService.StringToObj(data);

        
    };

    ProcessCMD[_CMD_.sc_private]                  = function sc_private(data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('OnPrivateMessage', [info]);
    };

    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_newbie_to_txt_channel] = function sc_join_newbie_to_txt_channel(data){
        // UID UINS Moders
        var users = mcService.StringToObj(data);
        var uid   = 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.onlineState = user.hasOwnProperty("onlineState") ? user.onlineState : 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_online_notify(data){
        var info = mcService.StringToObj(data);
        var user = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [info.UIN]);

        if (user){
            $rootScope.$broadcast('addCustomMSG', [{
                UIN     : info.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_users_changeid_data]      = function sc_users_changeid_data(data){
        var info = mcService.StringToObj(data);

        if (!customCallBack(_CMD_.cs_get_users_changeid_data, info) && !customCallBack(_CMD_.cs_get_fresh_users_data, info)) {
            customCallBack(_CMD_.cs_set_uin_info, info);
            
            $rootScope.$broadcast(window._messages_.usersSmallInfo.setAllUsersInfo, [info, true]);
        }
    };

    ProcessCMD[_CMD_.sc_common_options]           = function sc_common_options(data){
        // команда для обновления общих для всех пользователей сервера параметров,
        // все переменные - необязатльные, применять настройку, только если поле реально есть внутри JSON
        // {
        //     "MaxUploadFileSize"  : 10, // максимальный размер файла для заливки на сервер, 0 - без ограничений
        //     "MaxSelfMsgEditTime" : 15  // максимальное время редактирования собственных сообщений в конференциях и приватах, в минутах
        // }
        var info = mcService.StringToObj(data);

        mcConst.UserInfo.MaxSelfMsgEditTime = info.MaxSelfMsgEditTime !== void(0) ? info.MaxSelfMsgEditTime : mcConst.UserInfo.MaxSelfMsgEditTime;
        mcConst.UserInfo.MaxUploadFileSize  = info.MaxUploadFileSize !== void(0)  ? info.MaxUploadFileSize * 1024 * 1024 : mcConst.UserInfo.MaxUploadFileSize;

        // if (!customCallBack(_CMD_.cs_get_users_changeid_data, info) && !customCallBack(_CMD_.cs_get_fresh_users_data, info)) {
        //     customCallBack(_CMD_.cs_set_uin_info, info);
        //
        //     $rootScope.$broadcast(window._messages_.usersSmallInfo.setAllUsersInfo, [info, true]);
        // }
    };

    ProcessCMD[_CMD_.sc_set_remote_client_current_place_id] = function sc_set_remote_client_current_place_id(data){
//        "Show"  : true, // показать окно чата на передний план или нет
//        "Place" : 1,    // тип раздела чата, который нужно открыть
//        "ID"    : 17    // UIN, если нужно открыть приват, UID - если конференцию и т.п.
        var CONSOLE   = 0;
        var PRIVATE   = 1;
        var TEXT_CONF = 2;
        var FORUM     = 3;
        var KANBAN    = 4;
        var BBS       = 5;
        var BROADCAST = 6;
        var DEFAULT   = 7;
        var CREATE_NEW_TEXT_CONF = 8;
        var SELECT_CONF_TO_JOIN  = 9;
        var GET_ANDROID          = 10;
        var GET_IMPORTANT_MESSAGES = 11;
        var GET_IOS = 13

        var info = mcService.StringToObj(data);

        if (!$rootScope.isWebClient && info.Show){
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_show_on_top
            ]);
        }

        switch (info.Place){
            case CONSOLE:
            break;

            case PRIVATE:
                $rootScope.$broadcast('OpenPrivate', [info.ID]);
            break;

            case TEXT_CONF:
                $rootScope.$broadcast('OpenConf', [info.ID]);
            break;

            case FORUM:
                $rootScope.SetForum();
                $rootScope.$broadcast('selectTool');
                $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.Forum]);
            break;

            case KANBAN:
                $rootScope.SetKanban();
                $rootScope.$broadcast('selectTool');
                $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.Kanban]);
            break;

            case BBS:
                $rootScope.SetBBS();
                $rootScope.$broadcast('selectTool');
                $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.BBS]);
            break;

            case BROADCAST:
                $rootScope.SetSendBroadcast();
                $rootScope.$broadcast('selectTool');
                $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.SendBroadcast]);
            break;

            case DEFAULT:
            break;

            case CREATE_NEW_TEXT_CONF:
                $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.CreateConf]);
            break;

            case SELECT_CONF_TO_JOIN:
                $rootScope.$broadcast('show' + mcConst.dataModels.ConfManager, []);
            break;

            case GET_ANDROID:
            break;

            case GET_IMPORTANT_MESSAGES:
            break;

            case GET_IOS:
            break;
        }
    };

    /*deprecated*/ /*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_tech_kick(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_user_state]               = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.dialogsList.changeUserState, [info]);
    };

    ProcessCMD[_CMD_.sc_integrations]             = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast(window._messages_.chatFrame.on_sc_integrations, [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);

        if (!customCallBack(_CMD_.cs_get_options_preset, info)) {
            if ($rootScope.isWebClient){
                mcConst.ClientSettings = mcService.Marge(mcConst.ClientSettings, info);
            } else {
                $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);

        info.iRead = true;
        
        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_tech_message]             = function (data) {
        var info = mcService.StringToObj(data);

        info.isTechMsg = true;

        $rootScope.$broadcast('addCustomMSG', [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("on_conf_read", [info]);
    };

    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');
    };

    ProcessCMD[_CMD_.sc_media_conf]               = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('sc_media_conf', [info]);
    };

    ProcessCMD[_CMD_.sc_media_conf_leave]         = function (data){
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('media_conf_leave', [info]);
    };

    // ProcessCMD[_CMD_.sc_media_conf_join]          = function (data){
    //     var info = mcService.StringToObj(data);
    //
    //     $rootScope.$broadcast('sc_media_conf_join', [info]);
    // };

    ProcessCMD[_CMD_.sc_conf_status_change]          = function (data){
        // "UID"     : 17, // идентификатор конференции
        // "State"   : 0,  // статус, 0 - текстовая, 1 - аудио конференция, 2 - видеоконференция, 3 - вебинар
        // "Invoker" : 3,  // инициатор смены статуса
        // "Data"    : ""  // JSON кастомные настройки конференции, необязательно
        var info = mcService.StringToObj(data);

        $rootScope.$broadcast('conf_status_change', [info]);
    };

    // === 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]);
    };

    // Electron CMD =====================================================================

    eCmd[_CMD_.ec_server_added]                 = _CMD_.ce_add_server;
    eCmd[_CMD_.ec_server_saved]                 = _CMD_.ce_modify_server;
    eCmd[_CMD_.ec_get_client_settings]          = _CMD_.ce_get_client_settings;
    eCmd[_CMD_.ec_load_autoconnect_server]      = _CMD_.ce_load_autoconnect_server;
    eCmd[_CMD_.ec_storage_get]                  = _CMD_.ce_storage_get;
    eCmd[_CMD_.ec_statistics_get]               = _CMD_.ce_statistics_get;
    eCmd[_CMD_.ec_get_mc_client_info]           = _CMD_.ce_get_mc_client_info;
    eCmd[_CMD_.ec_ftp_list]                     = _CMD_.ce_ftp_list;
    eCmd[_CMD_.ec_history_get_dialogs]          = _CMD_.ce_history_get_dialogs;
    eCmd[_CMD_.ec_get_logs_list]                = _CMD_.ce_get_logs_list;
    eCmd[_CMD_.ec_get_user_folder]              = _CMD_.ce_get_user_folder;
    eCmd[_CMD_.ec_get_logs_files]               = _CMD_.ce_get_logs_files;
    eCmd[_CMD_.ec_file_set_new_folder_for_user] = _CMD_.ce_file_set_new_folder_for_user;
    eCmd[_CMD_.ec_file_direct_receive_start]    = _CMD_.ce_file_direct_receive_start;

    eCmd[_CMD_.ec_profile_get]                  = _CMD_.ce_profile_get;
    eCmd[_CMD_.ec_profile_save]                 = _CMD_.ce_profile_save;
    eCmd[_CMD_.ec_profile_remove]               = _CMD_.ce_profile_remove;
    eCmd[_CMD_.ec_encode_qrcode_json]           = _CMD_.ce_encode_qrcode_json;

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

    ProcessCMD[_CMD_.ec_drop_connect]      = ProcessCMD[_CMD_.sc_drop_connect];

    ProcessCMD[_CMD_.ec_get_server_list]   = function (info){
        $rootScope.$broadcast('addServerHostToList', [info]);

        customCallBack(_CMD_.ce_get_server_list, info);
    };

    ProcessCMD[_CMD_.ec_test_server_ip]    = function (info){
        $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

        customCallBack(_CMD_.ce_test_server_ip, info);
    };

    ProcessCMD[_CMD_.ec_complete_command]  = function sc_adm_command_complete(info){

        switch (info.Cmd){

            //ce_del_server

            default:
                customCallBack(info.Cmd);
        }
    };

    ProcessCMD[_CMD_.ec_error]             = function (data){
        runElectronError(mcService.StringToObj(data));
    };

    ProcessCMD[_CMD_.ec_open_dialog]       = function (data){
        if (data.itemID) {
            $rootScope.$broadcast(window._messages_.dialogsCtrl.selectDialog, [data.itemID.toString()]);
        }
    };

    ProcessCMD[_CMD_.ec_open_local_kanban] = function (url){
        $rootScope.$broadcast(window._messages_.dialogsCtrl.selectDialog, [mcConst.dataModels.Kanban]);
        $rootScope.$broadcast(window._messages_.kanban.openKanbanUrl, [url]);
    };

    ProcessCMD[_CMD_.ec_open_local_forum]  = function (url){
        $rootScope.$broadcast(window._messages_.forum.openForumUrl, [url]);
        $rootScope.$broadcast(window._messages_.dialogsCtrl.selectDialog, [mcConst.dataModels.Forum]);
    };

    ProcessCMD[_CMD_.ec_ftp_progress]      = function (data){
        $rootScope.$broadcast('onFtpProgress', [data]);
    };

    ProcessCMD[_CMD_.ec_main_window_state] = function (data){
        mcConst.isWindowFocused = data.state.focus;
        mcConst.mainWindowSate  = data;

        if (data.notify && data.state.focus){ // todo: если добавлю on_mainWindowBlur - убрать проверку data.state.focus и вернуть первую строку
            // $rootScope.$broadcast(data.state.focus ? 'on_mainWindowFocused' : 'on_mainWindowBlur');
            $rootScope.$broadcast('on_mainWindowFocused');
        }
    };

    ProcessCMD[_CMD_.ec_get_clipboard_files_list] = function (data){
        $rootScope.$broadcast('pastFilesFromClipboard', [data]);
    };

    ProcessCMD[_CMD_.ec_system_suspend]    = function (){
        $rootScope.systemSuspened = true;
    };

    ProcessCMD[_CMD_.ec_system_resume]     = function (){
        $rootScope.systemSuspened = false;

        $rootScope.$broadcast('ReconnectLoop');
    };

    ProcessCMD[_CMD_.ec_enter_to_idle]     = function (){
        if (mcConst.LoggedIn) {
            console.log("ec_enter_to_idle setstate away");

            mcConst.UserInfo.onlineState = mcConst.states.away;

            mcConnect.SendDataToServer({
                CMD  : _CMD_.cs_set_state,
                Data : mcConst.SessionID + mcConst.CR + mcConst.states.away
            });

            $rootScope.$broadcast(window._messages_.clientData.setClientTitle);
        }
    };

    ProcessCMD[_CMD_.ec_quit_from_idle]    = function (){
        if (mcConst.LoggedIn) {
            console.log("ec_quit_from_idle setstate online");

            mcConst.UserInfo.onlineState = mcConst.states.online;

            mcConnect.SendDataToServer({
                CMD  : _CMD_.cs_set_state,
                Data : mcConst.SessionID + mcConst.CR + mcConst.states.online
            });

            $rootScope.$broadcast(window._messages_.clientData.setClientTitle);
        }
    };

    ProcessCMD[_CMD_.ec_download_update]   = function (serverInfo){
        $rootScope.$broadcast('hide' + mcConst.lockInterface, ["all"]); // unlock
        $rootScope.$broadcast('hide' + mcConst.dataModels.Login, []);
        $rootScope.$broadcast(window._messages_.main.hideAllErrors);

        $rootScope.$broadcast('DownloadClientUpdate', [serverInfo]);

        $rootScope.$broadcast('hide' + mcConst.dataModels.ChatWrapper, []);
    };

    // ProcessCMD[_CMD_.ec_ftp_error]         = function (error){
    //     $rootScope.$broadcast(window._messages_.commonFiles.ftpError, [error]);
    // };

    ProcessCMD[_CMD_.ec_close_program]     = function (){
        if (((mcConst.LoggedIn && mcService.checkRights(mcConst._CMD_.RS.QAllowCloseProgram)) || !mcConst.LoggedIn) && !$rootScope.exitConfirmWindow){
            $rootScope.$broadcast('hide' + mcConst.lockInterface, ["all"]); // unlock
            $rootScope.$broadcast('hide' + mcConst.dataModels.Login, []);
            $rootScope.$broadcast(window._messages_.main.hideAllErrors);

            $rootScope.$broadcast(window._messages_.clientData.quitFromProgram);
        }
    };

    ProcessCMD[_CMD_.ec_logout]     = function (){
        $rootScope.$broadcast(window._messages_.clientData.logoutUser);
    };

    // ============= FILE Download / Upload ===================

    ProcessCMD[_CMD_.ec_file_check_exist]            = function (data){
        $rootScope.$broadcast('on_file_check_exist', [data]);
    };

    // -- download --

    ProcessCMD[_CMD_.ec_file_download_start]         = function (args){
        $rootScope.$broadcast('on_file_download_start', args);
    };

    ProcessCMD[_CMD_.ec_file_download_progress]      = function (info){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_download_progress, [info]);
        
        // $rootScope.$broadcast(window._messages_.downloadUpload.on_file_download_progress, [{
        //     percent: info.percent,
        //     size   : mcService.formatFileSize(info.total),
        //     text   : mcService.formatFileSize(info.trSize)
        // }]);

        // $rootScope.$broadcast('on_file_download_progress', [data]);
    };

    ProcessCMD[_CMD_.ec_file_download_complete]      = function (data){
        $rootScope.$broadcast('on_file_download_complete', [data]);
    };

    ProcessCMD[_CMD_.ec_file_download_aborted]       = function (data){
        $rootScope.$broadcast('on_file_download_aborted', [data]);
    };

    ProcessCMD[_CMD_.ec_file_download_file_not_found]= function (data){
        $rootScope.$broadcast('on_file_download_file_not_found', [data]);
    };

    // -- upload --

    ProcessCMD[_CMD_.ec_file_upload_error]           = function (data){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_upload_error, [data]);
    };

    ProcessCMD[_CMD_.ec_file_upload_start]           = function (data){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_upload_start, [data]);
    };

    ProcessCMD[_CMD_.ec_file_upload_prepare_start]   = function (data){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_upload_prepare_start, [data]);
    };

    ProcessCMD[_CMD_.ec_file_upload_prepare_progress]= function (data){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_upload_prepare_progress, [data]);
    };

    ProcessCMD[_CMD_.ec_file_upload_progress]        = function (data){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_upload_progress, [data]);
    };

    ProcessCMD[_CMD_.ec_file_upload_complete]        = function (data){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_upload_complete, [data]);
    };

    ProcessCMD[_CMD_.ec_file_upload_aborted]         = function (data){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_upload_aborted, [data]);
    };

    ProcessCMD[_CMD_.ec_file_direct_upload_no_files] = function (data){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_direct_upload_no_files, [data]);
    };

    ProcessCMD[_CMD_.ec_file_direct_upload_progress] = function (data){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_upload_progress, [data]);
    };

    ProcessCMD[_CMD_.ec_file_direct_upload_complete] = function (data){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_direct_upload_complete, [data]);
    };

    ProcessCMD[_CMD_.ec_file_direct_upload_start]    = function (data){
        $rootScope.$broadcast(window._messages_.downloadUpload.on_file_direct_upload_wait_accept, [data]);
    };

    ProcessCMD[_CMD_.ec_grant_access_media]    = function (data){
        $rootScope.$broadcast(window._messages_.callManager.grant_access_media, [data]);
    };

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

    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 + mcConst.CR + navigator.userAgent,

        });
    };

    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);
                }
            }

            if (!mcConst.PingTimer) {
                $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 = mcConst.UserInfo.hasInvite
            ? mcConst._CMD_.ClientType.GUEST
            : $rootScope.clientSysInfo
                ? $rootScope.clientSysInfo.Client
                : _CMD_.ClientType.WEB_CHAT;

        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 = $rootScope.clientSysInfo ? $rootScope.clientSysInfo.Client : _CMD_.ClientType.WEB_CHAT;

        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': 5,
            'timeout' : 20000
        });

        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)]);
        });

        mcConnect.SendDataToServer({
            CMD : _CMD_.cs_login_by_token,
            Data: {
                Token     : token,
                ClientType: 'chat',
                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];
}
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 (msg.Idx > confList.getLastMsgIdx(msg.UID)){
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_conf_got_message,
                        mcConst.SessionID,

                        msg.UID,
                        msg.Idx
                    ]);
                }

                if (msg.Idx > confList.getReadIdx(msg.UID) && read) {
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_conf_read_message,
                        mcConst.SessionID,

                        msg.UID,
                        msg.Idx
                    ]);
                }

                confList.addMessage(uid, msg.Idx, read);
            } else

            // =========== PRIVATES =================
            if (privateList.hasPrivate(msg.UINFrom)){
                read = (currentID == msg.UINFrom && currentType === 'UIN') && mcConst.isWindowFocused;

                if (msg.Idx > privateList.getGotIdx(currentID)) { // privateList.addMessage(msg.UINFrom, msg.Idx, read)
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_private_msg_got,
                        mcConst.SessionID,

                        msg.UINFrom,
                        msg.Idx
                    ]);
                }

                if (msg.Idx > privateList.getReadIdx(currentID) && read){
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_private_msg_read,
                        mcConst.SessionID,

                        currentID,
                        msg.Idx
                    ]);
                }

                privateList.addMessage(msg.UINFrom, msg.Idx, read);
            } 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);
            }

            confList.chengeStatuses(_state);

            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);
            }

            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',
        on_conf_read            : 'on_conf_read',
        
        confUpdateUserInfo      : 'confUpdateUserInfo',
        getUserPhoto            : 'getUserPhoto',
        getActiveProcess        : 'getActiveProcess',
        clearConfHistory        : 'clearConfHistory',
        clearPrivHistory        : 'clearPrivHistory',
        setPrivatDialogsIdx     : 'setPrivatDialogsIdx',
        on_mainWindowFocused    : 'on_mainWindowFocused',
        getConfHistoryMsg       : 'getConfHistoryMsg',
        getPrivatHistoryMsg     : 'getPrivatHistoryMsg',
        conf_status_change      : 'conf_status_change',
        getMyPrevMessagePriv    : 'getMyPrevMessagePriv',
        getMyPrevMessageConf    : 'getMyPrevMessageConf',

        getUserConfCount        : 'getUserConfCount',

        '----':'----'
    };

    $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();
        var cbOnOpen = args[1];

        $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 (cbOnOpen){
                privateList.cbOnOpen(uin, cbOnOpen);
            }

            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;

        if (privateList.getLastIdx(UIN) >= ID) {
            privateList.setMyMsgGot(UIN, ID);
        }

        setTimeout(function (){
            $rootScope.$broadcast("mark_private_got_notify", [UIN, ID]);
        }, 11);
    });

    $rootScope.$on(_msg.on_private_read_notify, function (e, args) {
        var UIN = args[0].UIN;
        var ID  = args[0].ID;

        if (privateList.getLastIdx(UIN) >= ID){
            if (args[0].iRead){
                privateList.setMsgRead(UIN, ID);

                var user = privateList.getPrivateInfo(UIN);

                if (user) {
                    $rootScope.$broadcast(window._messages_.dialogsCtrl.addOrUpdatePrivateDialog, [user]);
                } else {
                    console.warn("No user info for UIN: " + UIN);
                }
            } else {
                privateList.setMyMsgRead(UIN, ID);
            }
        }
    });

    $rootScope.$on(_msg.OnPrivateMessage, function(e, args){
        privateList.sendMessage.apply(null, args);
    });

    $rootScope.$on(_msg.getMyPrevMessagePriv, function(e, args){
        return privateList.getMyPrevMessage.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(e, args){
        // if (mcConst.UserInfo.PvDlgIdx) {
            privateList.getDialogsList(args[0] || null);
        // }
    });

    $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.getMyPrevMessageConf, function(e, args){
        return confList.getMyPrevMessage.apply(null, args);
    });

    $rootScope.$on(_msg.clearConfHistory, function(e, args){
        confList.clearHistory.apply(null, args);
    });

    $rootScope.$on(_msg.getUserConfCount, function(e, args){
        return confList.getUserCount.apply(null, args);
    });

    $rootScope.$on(_msg.on_conf_read, function(e, args){
        var UID    = args[0].UID;
        var ID     = args[0].ID;
        var lastID = confList.getLastMsgIdx(UID);

        // income read idx > saved last idx, replace last idx
        if (ID > lastID){
            confList.addMessage(UID, ID, true);
            lastID = ID;
        }

        confList.setMsgRead(UID, ID);

        var count = lastID - ID;

        $rootScope.$broadcast(window._messages_.dialogsCtrl.updateCounter, [{
            UID      : UID,
            IDMsgLast: lastID,
            IDMsgRead: ID,
        }, !count]);
    });

    $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);
    });

    $rootScope.$on(_msg.conf_status_change, function (e, args) {
         var info = args[0];
         var conf = confList.getConf(info.UID, true);

         conf.State = info.State;
         conf.Data  = info.Data;
    });

    // --- 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();
    });

    $rootScope.$on(_msg.getLastUserStates, function(e, args){
        var cb = args[0];

        if (cb) {
            cb(privateList.statuses.getAll());
        } else {
            return 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]);
        }
    });
}

/**
 * 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;
    var cbOpenList  = {};
    var myPrevMsg   = {};

    // __statuses.onStatesChange("privates", function (states) {
    //     Object.keys(__list).forEach(function (uin) {
    //         __setState(uin, states[uin]);
    //     });
    //
    // });

    function __getPrivate(uin, noCopy) {
        var res = null;

        if (__list.hasOwnProperty(uin)){
            res = noCopy ? __list[uin] : mcService.Marge({}, __list[uin]);
            // res = noCopy ? __list[uin] : structuredClone(__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;
    }

    function __cbOnOpen(uin, cb){
        if (!cbOpenList[uin] || (cbOpenList[uin] && !cbOpenList[uin].length)){
            cbOpenList[uin] = [cb];
        } else {
            cbOpenList[uin].push(cb);
        }
    }

    // === 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++;

                    __addPrivateMessage(uin, source[i].Idx);
                }

                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.onlineState,
                            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.onlineState,
                            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;

        Idx = parseInt(Idx);

        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 __setMsgGot(uin, idx) {
        if (__hasPrivate(uin)){
            __getPrivate(uin, true).IDMsgGot = idx;
        }
    }

    function __setMsgRead(uin, idx) {
        if (__hasPrivate(uin)){
            __getPrivate(uin, true).IDMsgRead = parseInt(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,
                    onlineState       : 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[0]);

                    __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,
                            onlineState  : msg.StateFrom,
                            Nick   : msg.Nick
                        }, msg), true]);
                    // }

                    _drawMsg(msg);
                }], mcConst.UserInfo.hasInvite);
            } 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,
                        onlineState   : msg.UINFrom == mcConst.UserInfo.UIN ? msg.StateTo : msg.StateFrom,
                        Nick    : __getPrivateNick(_uin)
                    }, msg), msg.upSync]);

                    __updatePrivate(_uin, {
                        MsgType : msg.MsgType,
                        MsgCut  : msgCut,
                        Speaker : msg.UINFrom,
                        onlineState   : msg.UINFrom == mcConst.UserInfo.UIN ? msg.StateTo : msg.StateFrom,
                        Nick    : __getPrivateNick(_uin),
                        dt      : msg.dtUTC
                    });
                }

                if (msg.UINFrom == mcConst.UserInfo.UIN && !msg.noUpdateCounter && msg.MsgType === mcConst._CMD_.msgType.TEXT && !msg.History) {
                    myPrevMsg[msg.UINTo] = msg;
                }

                _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 = {};

                // get profile info from storage
                if (!$rootScope.isWebClient){
                    Q.AddTask(function () {
                        $rootScope.$broadcast('sendCMDToElectron', [
                            mcConst._CMD_.ce_profile_get,
                            $rootScope.UNIT.ctrlServersManager.getServerIdByServID(mcConst.ClientSettings.ServID),
                            uin,

                            function (data) {
                                profile = data;

                                Q.Next();
                            }
                        ]);
                    });
                }

                // 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 : parseInt(data.IDMsgGot),
                        IDMyMsgRead: parseInt(data.IDMsgRead)
                    });

                    $rootScope.$broadcast("updateMessagesState", [data.UIN]);
                }
            ]);

            $rootScope.SetPrivate(uin);

            if (!$rootScope.isWebClient){
                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_hide_notify
                ]);
            }
        }

        var lastID = __getBottomInListMsgIdx(uin);
        var readID = __getPrivate(uin, true).IDMsgRead;

        if (lastID > 0 && lastID != readID && mcConst.isWindowFocused){
            __getPrivate(uin, true).IDMsgRead = parseInt(lastID);

            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_private_msg_read,
                mcConst.SessionID,

                uin,
                lastID
            ]);
        }

        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);
            }
        }

        if (cbOpenList[uin] && cbOpenList[uin].length){
            (cbOpenList[uin].shift())();
        }
    }

    function __addPrivate(userPrivate, notLoadAvatar) {
        var res = false;

        if (userPrivate && userPrivate.UIN != mcConst.UserInfo.UIN){
            if (__hasPrivate(userPrivate.UIN)){
                __updatePrivate(userPrivate.UIN, userPrivate);
                
                userPrivate = __getPrivate(userPrivate.UIN, true); //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;
                };

                __list[userPrivate.UIN] = userPrivate;
            }

            userPrivate.DisplayName = userPrivate.DisplayName || userPrivate.Nick;
            userPrivate.myMsg       = {
                IDMyMsgRead : userPrivate.IDMyMsgRead || 0,
                IDMyMsgGot  : userPrivate.IDMyMsgGot  || 0
            };

            if (userPrivate.hasOwnProperty('State')){
                __statuses.setUserState(userPrivate.UIN, userPrivate.State);
            } else
            if (userPrivate.hasOwnProperty('onlineState')){
                __statuses.setUserState(userPrivate.UIN, userPrivate.onlineState);
            } else {
                userPrivate.onlineState = __statuses.getUserState(userPrivate.UIN);
            }

            /*$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)]);

            if (!notLoadAvatar) {
                $rootScope.$broadcast(window._messages_.avatarsManager.showAvatars, [userPrivate.UIN, mcConst.avatars.MEDIUM]);
            }
        }

        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).onlineState = __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) {
                task.Next();
            } else {
                var uins = [];

                userList.Data.forEach(function (user) {
                    uins.push(user.UIN);
                });

                $rootScope.$broadcast(window._messages_.usersSmallInfo.getInfoByUserUINs, [
                    uins.join(','),

                    function (_list) {
                        userList.Data.forEach(function (user, idx) {
                            // var findUser = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [user.UIN]);
                            var findUser = _list[idx];

                            if (findUser) {
                                user.DisplayName = findUser.DisplayName;
                                user.Avatar = findUser.Avatar;
                            } 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);
                    },

                    mcConst.UserInfo.hasInvite
                ]);
            }
        });

        // 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;
            var uins = [];

            __closeAllPrivates();

            if (userList.Data.length){
                userList.Data.forEach(function (user) {
                    if (user) {
                        user.onlineState = __statuses.getUserState(user.UIN);//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, true); // true - not load avatars

                        if (user.Avatar && user.UIN) {
                            uins.push(user.UIN);
                        }
                    }
                });

                $rootScope.$broadcast(window._messages_.avatarsManager.showAvatars, [uins, mcConst.avatars.MEDIUM]);
            } 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;
    }

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

    function __getMyPrevMessagePriv(uin) {
        return myPrevMsg[uin];
    }

    this.setAllMsgRead = __setAllMsgRead;

    this.showPrivate = __showPrivate;

    // Open on PrivateRequest
    this.addPrivate = function (info) {
        // $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 (_user) {
                    // info.DisplayName = __getPrivate(info.UIN, true).DisplayName = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [info.UIN]).DisplayName;
                    info.DisplayName = __getPrivate(info.UIN, true).DisplayName = _user[0].DisplayName;

                    __showPrivate(info.UIN);
                }, mcConst.UserInfo.hasInvite]);
            }
        // }]);
    };

    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.onlineState !== void(0)) {
                    __setState(uin, newData.onlineState);
                }

                // 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.getMyPrevMessage = __getMyPrevMessagePriv;

    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.getGotIdx = __getLastGotMsgIdx;

    this.setMyMsgGot = __setMyMsgGot;

    this.setMsgGot = __setMsgGot;

    this.setMsgRead = __setMsgRead;

    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.cbOnOpen = __cbOnOpen;

    this.setBackupIdx = function (id) {
        backupDlgId = id;
    }
}

/**
 * Created by Gifer on 22.08.2017.
 */

function MCConferenceList($rootScope, _drawMessage, __statuses, mcPlaySound) {
    "use strict";

    var __list     = {};
    var historyMsg = {};
    var megaIdx    = Number.MAX_SAFE_INTEGER; // 999999999;
    var myPrevMsg  = {};

    // __statuses.onStatesChange("confs", function () {
    //     __getConfList().forEach(__calcOnline);
    // });
    //
    // function __calcOnline(uid, userState){
    //     var conf = __getConf(uid, true);
    //
    //     conf.onlineCount = 0;
    //
    //     if (userState !== void(0)){
    //         userState >=0 ? conf.onlineCount++ : conf.onlineCount--;
    //     } else
    //     __getConfUsers(uid, true).forEach(function (user) {
    //         if (__statuses.getUserState(user.UIN) !== mcConst.states.offline) {
    //             conf.onlineCount ++;
    //         }
    //     });
    //
    //     console.log('uid: ' + uid + ' online: ' + conf.onlineCount);
    //
    //     $rootScope.$broadcast(window._messages_.dialogsCtrl.updateChUserCount, [uid, conf.onlineCount]);
    // }

    function __hasConf(uid){
        return __list.hasOwnProperty(uid);
    }

    function __getConfList() {
        return Object.keys(__list) || [];
    }

    function __getConf(uid, noCopy) {
        var res = null;

        if (__hasConf(uid)){
            res = noCopy ? __list[uid] : mcService.Marge({}, __list[uid]);
        } /*else {
            console.warn("getConf: Conference list hasn't a conf with UID: " + uid);
        }*/

        return res;
    }

    function __getMaxMessageListIdx(conf){
        var res = 0;

        if (conf && conf.messagesList) {
            conf.messagesList.forEach(function (msgId){
                if (msgId > res){
                    res = msgId;
                }
            });
        }

        return res;
    }

    function __getConfUsers(uid, noCopy) {
        var res = [];

        if (__hasConf(uid)){
            if (noCopy){
                res = __list[uid].users;
            } else {
                // __list[uid].users.forEach(function (user) {
                //     res.push(mcService.Marge({}, user));
                // });
                
                res = mcService.Marge([], __list[uid].users);
                // res = structuredClone(__list[uid].users);
            }
        } else {
            console.warn("getConfUsers: Conference list hasn't a conf with UID: " + uid);
        }

        return res;
    }

    function __getUser(uid, uin, noCopy) {
        var res = null;

        if (__hasConf(uid)){
            var _idx = mcService.findItemInArrayOfObj(__list[uid].users, uin, "UIN");

            if (_idx !== -1){
                res = noCopy ? __list[uid].users[_idx] : mcService.Marge({}, __list[uid].users[_idx]);
            } else {
                console.warn("__getUser: can't find user: \"" + uin + "\" in conf UID: " + uid);
            }
        } else {
            console.warn("__getUser: Conference list hasn't a conf with UID: " + uid);
        }

        return res;
    }

    function __getConfName(uid) {
        // return __list[uid].Name || "";
        return __list[uid].ConfName || "";
    }

    function __findDirka(conf){
        var dirkas = [];
        var start = 0;

        for (var i = conf.messagesList.length - 1; i > 0; i--){
            if (conf.messagesList[i] - conf.messagesList[i -1] > 1 && !start){
                start = conf.messagesList[i];
            } else
            if (start && conf.messagesList[i] - conf.messagesList[i -1] === 1) {
                dirkas.push(start + "-" + conf.messagesList[i]);

                start = 0;
            }
        }
    }

    function __showConf(uid, pass){
        if (__hasConf(uid)){
            var conf = __getConf(uid, true);

            $rootScope.SetConf(uid);

            $rootScope.$broadcast('selectTool',       [mcConst.dataModels.ConfUserList]);
            $rootScope.$broadcast('fillChatFrame',    [conf.ConfName, conf.ConfTopic, conf]);
            $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.ChatFrame]);

            var lastID = conf.ConvID;
            var gotID  = __getMaxMessageListIdx(conf) || conf.IDMsgGot;// ? conf.IDMsgGot : conf.messagesList[conf.messagesList.length - 1];

            if (!conf.messagesList.length || !conf.hasHistory) { // no history need load
                __getConfHistory(uid);
            } else {
                var delta = lastID - gotID ;//+ (conf.hasHistory ? 0 : mcConst.countHistoryMessagesLoad);

                if (delta > 0) {
                    if (delta > 200) {
                        __clearHistory(uid, 0);
                        
                        delta = 50;
                    }

                    __getConfHistory(uid, delta);
                } else

                if (conf.hasDirka){

                }
            }

            conf.IDMsgGot = conf.ConvID;

            if (lastID > 0 && conf.IDMsgRead != conf.IDMsgGot && mcConst.isWindowFocused){
                conf.IDMsgRead = lastID;

                $rootScope.SendCMDToServer([
                    mcConst._CMD_.cs_conf_read_message,
                    mcConst.SessionID,

                    uid,
                    lastID
                ]);
            }
        } else {
            $rootScope.$broadcast('SendCMDToServer', [
                mcConst.lockInterface,
                mcConst._CMD_.cs_join_txt_channel,
                mcConst.SessionID,
                uid,
                pass || "",

                function (data) {
                    $rootScope.$broadcast("selectDialog", ["UID-" + data.UID]);
                }
            ]);
        }
    }

    function __addConf(newConf){
        var uid  = newConf.UID;
        var conf = __getConf(uid, true);

        if (newConf.UsersList){
            // newConf.users = newConf.UsersList;

            delete newConf.UsersList;
        }

        if (!conf){
            __list[uid] = conf = mcService.Marge(newConf, {
                users       : [],
                messagesList: [],
                hasHistory  : false,
                hasDirka    : false,
                IDMsgGot    : newConf.IDMsgGot  ? newConf.IDMsgGot  : newConf.ConvID,
                IDMsgRead   : newConf.IDMsgRead ? newConf.IDMsgRead : newConf.ConvID,
                ConvID      : newConf.ConvID    ? newConf.ConvID    : 0,
                firstConvID : megaIdx,
                hasMsgIdx   : function (idx) {
                    return this.messagesList.indexOf(idx) !== -1;
                }
            });
        } else {
            __list[uid] = conf = mcService.Marge(conf, newConf);
        }

        __list[uid].onlineCount = 0;

        if ((conf.IDMsgGot < conf.ConvID || conf.IDMsgGot == -1) && conf.ConvID > 0){
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_conf_got_message,
                mcConst.SessionID,

                uid,
                conf.ConvID
            ]);
        }

        var needSelectConf = __getConfList().length === 1 || newConf.OpenNow;
        var newMsgCount    = conf.ConvID - (conf.IDMsgRead == -1 ? conf.IDMsgGot : conf.IDMsgRead);

        $rootScope.$broadcast(window._messages_.dialogsCtrl.addOrUpdateConfDialogs, [__getConf(uid)]);

        if (newMsgCount) {
            $rootScope.$broadcast(window._messages_.chatFrame.showNotifyMessage, [true, mcLang(722, [conf.ConfName]) + newMsgCount, "", true, uid]); // "722":"Новые сообщения в конференции \"%s\": ",
        }
        
        if ($rootScope.GetChatTypeID() === "UID-" + uid) {
            __showConf(uid);
        } else
        if (needSelectConf && $rootScope.GetChatType() !== $rootScope.chatAliases.BBS) {
            $rootScope.$broadcast('selectDialog',["UID-" + uid]);
        }

        // conf.IDMsgGot = conf.ConvID;
    }

    function __removeConf(uid) {
        if (__hasConf(uid)){
            delete __list[uid];

            return true;
        }
    }

    function __addUsersToConf (uid, _users, _uins){
        var conf     = __getConf(uid, true);

        if (conf){
            var userList = __getConfUsers(uid, true);
            var isEmpty  = false;

            _uins = _uins.split(',');

            if (!userList.length){
                isEmpty = true;
            }

            // if (count>1) ___native_console.profile(uid + " " + count);

            _users.forEach(function(user, id){
                var currentUser = null;

                if (!user) {
                    _uins[id] != 0 && console.log('warning user id: ' + id + ' uin: ' + _uins[id]);

                    if (_uins[id] == 0) {
                        currentUser  =  $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [0])  ||{
                            UIN        : 0,
                            DisplayName: mcConst.ElisaName,
                            TeamLead   : true,
                            Sex        : -1,
                            ChangeID   : 0,
                            Avatar     : 0
                        };
                    } else {
                        return;
                    }
                } else {
                    currentUser  = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [user.UIN]);
                }

                currentUser.id    = currentUser.UIN.toString();

                if (!currentUser.DisplayName){
                    currentUser.DisplayName = currentUser.Nick;
                }

                if (isEmpty){
                    userList.push(currentUser);

                    if (__statuses.getUserState(currentUser.UIN) >=0 ){
                        conf.onlineCount ++;
                    }

                } else {
                    var idx = mcService.findItemInArrayOfObj(userList, currentUser.UIN, 'UIN');

                    if (idx < 0){
                        userList.push(currentUser);
                    } else {
                        userList[idx] = currentUser;
                    }

                    if (__statuses.getUserState(currentUser.UIN) >=0 ){
                        conf.onlineCount ++;
                    } else
                    if (conf.onlineCount > 0) {
                        conf.onlineCount --;
                    }
                }

                // _users.shift();
            });

            // if (count>1) ___native_console.profileEnd(uid + " " + count);

            if ($rootScope.GetChatID() === uid && $rootScope.GetChatType() === $rootScope.chatAliases.UID){
                if (isEmpty){
                    $rootScope.$broadcast(window._messages_.confUsers.addUserToConf, [mcService.Marge([], userList)]);
                    // $rootScope.$broadcast(window._messages_.confUsers.addUserToConf, [structuredClone(userList)]);
                } else {
                    $rootScope.$broadcast(window._messages_.confUsers.addUserToConf, [__getConfUsers(uid)]);
                }
            }

            if ($rootScope.isWebClient || mcConst.ClientSettings.SoundsSndJoin) {
                mcPlaySound.PlaySound(mcPlaySound.Sounds.JoinConf);
            }

            $rootScope.$broadcast(window._messages_.dialogsCtrl.updateChUserCount, [uid, conf.onlineCount]);
        } else {
            console.log('__addUsersToConf: uid=' + uid + ' not in list');
        }
    }

    function __removeUsersFromConf(uid, uins){
        uins = uins.toString().split(',');
        var conf = __getConf(uid, true);

        if (conf){
            var userList = __getConfUsers(uid, true);
            var uin      = "0";

            for (var i = 0; i < uins.length; i ++) {
                uin = uins[i];

                userList.splice(mcService.findItemInArrayOfObj(userList, uin, 'UIN'), 1);

                if (__statuses.getUserState(uin) >=0 ){
                    conf.onlineCount ++;
                } else
                if (conf.onlineCount > 0) {
                    conf.onlineCount --;
                }

                if ($rootScope.GetChatID() == uid && $rootScope.GetChatType() === $rootScope.chatAliases.UID) {
                    $rootScope.$broadcast(window._messages_.confUsers.removeUserFromConf, [uin.toString()]);
                }

                if ($rootScope.isWebClient || mcConst.ClientSettings.SoundsSndLeave) {
                    mcPlaySound.PlaySound(mcPlaySound.Sounds.LeaveConf);
                }

                if (uin == mcConst.UserInfo.UIN){
                    $rootScope.$broadcast(window._messages_.dialogsCtrl.removeConfFromDialogs, [uid]);

                    __removeConf(uid);

                    break;
                } else {
                    $rootScope.$broadcast(window._messages_.dialogsCtrl.updateChUserCount, [uid, conf.onlineCount]);
                }
            }
        }
    }

    function __leaveConf(uid){
        if (__hasConf(uid)) {
            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_leave_txt_channel,
                mcConst.SessionID,
                uid
            ]);
        }
    }

    function __getLastReadMsgIdx(uid) {
        return __hasConf(uid) ? __list[uid].IDMsgRead : -1;
    }

    function __getLastGotMsgIdx(uid) {
        return __hasConf(uid) ? __list[uid].IDMsgGot : -1;
    }

    function __getConfHistory(uid, count, more){
        if (__hasConf(uid) && ($rootScope.isWebClient || (mcConst.ClientSettings.SysEventsLoadHistoryType && mcConst.ClientSettings.LoadHistoryToConf))) {
            __list[uid].hasHistory = true;

            if (more){
                var range  = (__getFirstMsgIdx(uid) - mcConst.countHistoryMessagesLoad) + "-" + (__getFirstMsgIdx(uid) - 1);
                var _count = __getFirstMsgIdx(uid) - 1 - (__getFirstMsgIdx(uid) - mcConst.countHistoryMessagesLoad);

                $rootScope.$broadcast('SendCMDToServer' , [
                    mcConst._CMD_.cs_sync_conf_history,
                    mcConst.SessionID,

                    uid,
                    range,

                    function (data) {
                        data.Users = mcService.convertArrayToObj(data.Users, "UIN");

                        $rootScope.$broadcast("syncHistoryDone", ["UID-" + uid]);

                        var msgCnt  = data.Data.length;
                        var deleted =__showConfHistory(data, false, more);

                        if ((deleted > _count/3) && msgCnt !== 0){
                            __getConfHistory(uid, _count < deleted ? 0 : _count - deleted, true);
                        }
                    }
                ]);
            } else {
                $rootScope.$broadcast('SendCMDToServer' , [
                    mcConst._CMD_.cs_get_channel_history_messages,
                    mcConst.SessionID,

                    uid,
                    mcConst.historyTypes.count,                // Type
                    count || mcConst.countHistoryMessagesLoad, // Last
                    0,                                         // From
                    0,                                         // To
                    "",                                        // List

                    function (data) {
                        $rootScope.$broadcast("syncHistoryDone", ["UID-" + uid]);

                        var msgCnt  = data.Data.length;
                        var deleted = __showConfHistory(data, !!count);

                        if ((deleted > (count || mcConst.countHistoryMessagesLoad)/3) && msgCnt !== 0){
                            __getConfHistory(uid, (count || mcConst.countHistoryMessagesLoad) - deleted, true);
                        }
                    }
                ]);
            }
        } else {
            $rootScope.$broadcast("syncHistoryDone", ["UID-" + uid]);
        }
    }

    function __getHistoryMsg(_uid, _idx){
        var uid = _uid.toString();
        var idx = _idx.toString();

        return historyMsg[uid]
            ? historyMsg[uid][idx] ? mcService.StringToObj(historyMsg[uid][idx]) : null
            : null;
    }

    function __addMessageToHistory(msg){
        if (!historyMsg[msg.UID]){
            historyMsg[msg.UID] = {};
        }

        historyMsg[msg.UID][msg.Idx] = JSON.stringify(mcService.Marge({dtUTC: mcService.utcTime(msg, true)}, msg)); // сохраняем месадж для реплая
    }

    function __showConfHistory(source, notHistory, more){
        var messages   = mcService.isString(source.Data) ? JSON.parse(source.Data.replace(/\r\n/g, "\\r\\n")) : source.Data;
        var uid        = source.UID;
        var needMore   = 0;

        if (mcService.isArray(source.Users)) {
            source.Users = mcService.convertArrayObjToObj(source.Users, "UIN");
        }

        // if (source.Users && !mcService.isObjectEmpty(source.Users)) {
        //     $rootScope.$broadcast(window._messages_.usersSmallInfo.setAllUsersInfo, [source.Users]);
        // }

        while (messages.length) {
            var msg = messages[!notHistory ? "pop" : "shift"]();

            if (msg.MsgType === mcConst._CMD_.msgType.ADM_DELETE_MESSAGE || msg.Mod === mcConst._CMD_.msgMods.DELETED){
                needMore++;
            }

            if (msg.Mod === mcConst._CMD_.msgMods.DELETED || msg.MsgType === mcConst._CMD_.msgType.ADM_DELETE_MESSAGE) {
                __addMessage(uid, msg.Idx || msg.MsgIdx);
            } else
            if (!__hasMessage(uid, msg.Idx || msg.MsgIdx)) {

                msg.DisplayName  = msg.DisplayName ? msg.DisplayName : (source.Users && source.Users[msg.UIN] ? source.Users[msg.UIN].DisplayName || "noName" : "noName");
                msg.Idx          = msg.Idx || msg.MsgIdx;
                msg.UIN          = msg.UIN.toString();
                msg.Nick         = msg.DisplayName;
                msg.UID          = uid;
                msg.dt           = mcService.utcTime(msg);
                msg.noScrollDown = more;
                msg.History      = !notHistory;

                if (msg.idx === 1 && __getConf(uid, true).IDMsgRead < msg.idx){
                    msg.showNotify = true;
                }

                __addMessageToHistory(msg);
                _drawMessage(msg); // , true
            }
        }

        return needMore;
    }

    function __confMessage(msg) {
        var UID = msg.UID;

        if (__hasConf(UID) && !__hasMessage(UID, msg.Idx || msg.MsgIdx)){
            // var userList = __getConfUsers(UID, true);
            // var user = userList[mcService.findItemInArrayOfObj(userList, msg.UIN, 'UIN')];
            var user = __getUser(UID, msg.UIN);
            // var conf = __getConf(UIN, true);

            if (user){
                msg.Nick = user.DisplayName;
                msg.Idx  = msg.Idx || msg.MsgIdx;
                msg.UIN  = msg.UIN.toString();
                msg.dt   = mcService.utcTime(msg);

                if (msg.UIN != mcConst.UserInfo.UIN){
                    if (!mcService.inArrayB(msg.MsgType, [
                        mcConst._CMD_.msgType.ADM_DELETE_MESSAGE,
                        mcConst._CMD_.msgType.PRIVATE_REDIRECT,
                        mcConst._CMD_.msgType.BEEP])
                    ) {
                        $rootScope.$broadcast('updateCounter', [msg]);
                    }
                }

                __addMessageToHistory(msg);
                _drawMessage(msg);

                if (msg.UIN == mcConst.UserInfo.UIN && !msg.noScrollDown && msg.MsgType === mcConst._CMD_.msgType.TEXT && !msg.History) {
                    myPrevMsg[msg.UID] = msg;
                }

                // conf.IDMsgGot = msg.Idx;

                if (msg.MsgType !== mcConst._CMD_.msgType.ADM_DELETE_MESSAGE && ($rootScope.isWebClient || mcConst.ClientSettings.SoundsSndChat)){
                    mcPlaySound.PlaySound(mcPlaySound.Sounds.ConfMsg);
                }
            }
        }
    }

    function __hasMessage(uid, idx) {
        var res  = __getConf(uid, true);

        if (res){
            res = res.hasMsgIdx(idx);
        }

        return res;
    }

    function __addMessage(uid, idx, read) {
        var res = false;

        if (__hasConf(uid)){
            var conf = __getConf(uid, true);

            if (!conf.hasDirka && conf.messagesList.length && (idx - conf.messagesList[conf.messagesList.length - 1] > 1)){
                conf.hasDirka = true;
            }

            if (conf.messagesList.indexOf(idx) === -1) {
                conf.messagesList.push(idx);
            }

            if (conf.ConvID < idx){
                conf.ConvID   = idx;
                conf.IDMsgGot = idx;

                if (read){
                    conf.IDMsgRead = idx;
                }

                res = true;
            }
            // if (conf.lastConvID <= idx){
            //     conf.lastConvID = idx;
            //
            //     res = true;
            // }

            if (conf.firstConvID >= idx){
                conf.firstConvID = idx;
            }
        }

        return res;
    }

    function __getLastMsgIdx(uid) {
        // return __hasConf(uid) ? __list[uid].lastConvID : 0;
        return __hasConf(uid) ? __list[uid].ConvID : 0;
    }

    function __getFirstMsgIdx(uid) {
        return __hasConf(uid) ? __list[uid].firstConvID : megaIdx;
    }

    function __clearUserList() {
        for (var uid in __list){
            __list[uid].users     = [];
            __list[uid].IDMsgGot  = -1;
            __list[uid].IDMsgRead = -1;
            __list[uid].ConvID    = -1;
            __list[uid].dtUTCLastMsg   = mcConst.neverDate;
            __list[uid].LastSpeaker    = -1;
            __list[uid].LastMsgCutText = "";
            __list[uid].LastMsgType    = -1;
        }

        // __list = {};
    }

    function __updateUserInfo(user) {
        if (user) {
            __getConfList().forEach(function (uid) {
                var conf = __getConfUsers(uid, true);
                var id   = mcService.findItemInArrayOfObj(conf, user.UIN, "UIN");

                if (id >= 0){
                    conf[id] = mcService.Marge(conf[id], user);
                }
            });
        }
    }

    function __clearHistory(uid, leave) {
        if (__hasConf(uid)) {
            var conf = __getConf(uid, true);

            conf.messagesList = conf.messagesList.sort(function(a,b){return mcService.sortAB(b,a)});

            var delList = [];

            if (leave) {
                delList           = mcService.copy(conf.messagesList, leave);
                conf.messagesList = mcService.copy(conf.messagesList, 0, leave - 1);
                conf.firstConvID  = conf.messagesList[conf.messagesList.length - 1];
            } else {
                delList           = conf.messagesList;
                conf.messagesList = [];
                conf.firstConvID  = megaIdx;
            }
            
            $rootScope.$broadcast(window._messages_.chatFrame.clearConfMessages, [uid, delList]);
        }
    }

    function __setAllMsgRead(uid) {
        var conf = __getConf(uid);

        if (conf && conf.IDMsgRead != conf.IDMsgGot)
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_conf_read_message,
                mcConst.SessionID,

                uid,
                __getLastGotMsgIdx(uid)
            ]);
    }

    function __setMsgRead(uid, id){
        if (__hasConf(uid)){
            __getConf(uid, true).IDMsgRead = id;
        }
    }

    function __changeStatuses(_states) {
        var sts = mcService.getStateStatuses(_states);

         Object.keys(__list).forEach(function (uid) {
             var hasChanges = false;

             if (sts.Offline.length < __list[uid].users.length){
                 sts.Offline.forEach(function (uin) {
                     if (mcService.findItemInArrayOfObj(__list[uid].users, uin, "UIN") >= 0){
                         hasChanges = true;

                         if (__list[uid].onlineCount > 0 && __statuses.getUserState(uin) !== mcConst.states.offline ){
                             __list[uid].onlineCount --;
                         }
                     }
                 });
             } else {
                 __list[uid].users.forEach(function (uin) {
                     if (sts.Offline.indexOf[uin] >= 0){
                         hasChanges = true;

                         if (__list[uid].onlineCount > 0 && __statuses.getUserState(uin) !== mcConst.states.offline ){
                             __list[uid].onlineCount --;
                         }
                     }
                 });
             }

             if (sts.Online.length < __list[uid].users.length){
                 sts.Online.forEach(function (uin) {
                     if (mcService.findItemInArrayOfObj(__list[uid].users, uin, "UIN") >= 0){
                         hasChanges = true;

                         if (__statuses.getUserState(uin) === mcConst.states.offline ){
                             __list[uid].onlineCount ++;
                         }
                     }
                 });
             } else {
                 __list[uid].users.forEach(function (uin) {
                     if (sts.Online.indexOf[uin] >= 0){
                         hasChanges = true;

                         if (__statuses.getUserState(uin) === mcConst.states.offline ){
                             __list[uid].onlineCount ++;
                         }
                     }
                 });
             }

             if (hasChanges){
                 $rootScope.$broadcast(window._messages_.dialogsCtrl.updateChUserCount, [uid, __list[uid].onlineCount]);
             }
         });
    }

    function __getUserCount(uid){
        var res = 0;

        if (__hasConf(uid)){
            res = __list[uid].users.length;
        }

        return res;
    }

    function __getMyPrevMessageConf(uid) {
        return myPrevMsg[uid];
    }

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

    this.getMyPrevMessage = __getMyPrevMessageConf;

    this.clearHistory       = __clearHistory;

    this.showConf           = __showConf;

    this.addConf            = __addConf;

    this.addUsersToConf     = __addUsersToConf;

    this.removeUsersFromConf= __removeUsersFromConf;

    this.leaveConf          = __leaveConf;

    this.getConfHistory     = __getConfHistory;

    this.showConfHistory    = __showConfHistory;

    this.confMessage        = __confMessage;

    this.getConf            = __getConf;

    this.clearUserList      = __clearUserList;

    this.getHistoryMsg      = __getHistoryMsg;

    this.hasMessage         = __hasMessage;

    this.addMessage         = __addMessage;

    this.getConfList        = __getConfList;

    this.getConfUsers       = __getConfUsers;

    this.getConfName        = __getConfName;

    this.setMsgRead         = __setMsgRead;

    this.getReadIdx         = __getLastReadMsgIdx;

    this.getLastMsgIdx      = __getLastMsgIdx;

    this.getGotIdx          = __getLastGotMsgIdx;

    this.updateUserInfo     = __updateUserInfo;

    this.setAllMsgRead      = __setAllMsgRead;

    this.chengeStatuses     = __changeStatuses;

    this.getUserCount       = __getUserCount;
}

/**
 * Created by Gifer on 22.08.2017.
 */

function MCStatusesList($rootScope) {
    var __userOnlineStatuses = {};
    var __onChange = {};
    var __onlineCount = 0;

    function __clearState() {
        __userOnlineStatuses = {};
    }

    function __getAllStates() {
        return mcService.Marge({}, __userOnlineStatuses);
        // return structuredClone(__userOnlineStatuses);
    }

    function __setUserState(uin, state) {
        if (uin.toString() === '0'){
            __userOnlineStatuses[uin] = mcConst.states.online;
            __onlineCount ++;
        } else {
            if (state === mcConst.states.offline) {
                if (__onlineCount) __onlineCount --;

                // if (__onlineCount < 0){
                //     console.warn('__setUserState onlineCount <0 for uin: ' + uin);
                // }
            } else
            if (!__userOnlineStatuses[uin] || __userOnlineStatuses[uin] === mcConst.states.offline){
                __onlineCount ++;
            }

            __userOnlineStatuses[uin] = state === undefined ? mcConst.states.offline : parseInt(state);
        }

        mcService.setStatusForUser(uin, __userOnlineStatuses[uin]);

        return __userOnlineStatuses[uin];
    }

    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 __getOnlineCount (){
        return __onlineCount;
    }

    function __loadUserStatuses(){
        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_get_online_users_states,
            mcConst.SessionID,

            function (states){
                // "Free" : "1,7,14",  // список идентификаторов пользователей в статусе "Свободен", опционально, если такие пользователи есть в наличии
                // "Away" : "2,893,4", // список идентификаторов пользователей в статусе "Нет на месте", опционально, если такие пользователи есть в наличии
                // "DND"  : "13"       // список идентификаторов пользователей в статусе "Не беспокоить", опционально, если такие пользователи есть в наличии

                __clearState();

                if (states.Free){
                    states.Free = states.Free.split(',');

                    states.Free.forEach(function (uin) {
                        __setUserState(uin, mcConst.states.online);
                    });
                }

                if (states.Away){
                    states.Away = states.Away.split(',');

                    states.Away.forEach(function (uin) {
                        __setUserState(uin, mcConst.states.away);
                    });
                }

                if (states.DND) {
                    states.DND  = states.DND.split(',');

                    states.DND.forEach(function (uin) {
                        __setUserState(uin, mcConst.states.dnd);
                    });
                }

                __setUserState(0, mcConst.states.online);

                // var _state = __getAllStates();
                //
                // Object.keys(__onChange).forEach(function (name) {
                //     __onChange[name](_state);
                // });
                //
                // $rootScope.$broadcast('allUsersOnlineStatusesChanged', [_state]);
            }
        ]);
    }

    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;

    this.getOnlineCount = __getOnlineCount;
}

/**
 * Created by Gifer on 22.08.2017.
 */

function MCHistoryDialogsList($rootScope) {
    "use strict";

    var __list = {};

    function getList() {
        var res = {};

        Object.keys(__list).forEach(function (uin) {
            res[uin] = mcService.Marge({}, __list[uin]);
            // res[uin] = structuredClone(__list[uin]);
        });

        return res;
    }

    function getItem(uin) {
        var res = null;

        if (__list.hasOwnProperty(uin)){
            res = mcService.Marge({}, __list[uin]);
            // res = structuredClone(__list[uin]);
        }

        return res;
    }

    function updateItem(uin, name, dt) {
        if (!__list.hasOwnProperty(uin)){
            __list[uin] = {
                DisplayName  : name,
                UIN          : uin
            };
        }

        __list[uin].dtLastMessage = dt;
    }

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

    this.load = function () {
        $rootScope.Storage.historyDialogs.load(mcConst.UserInfo.UIN, mcConst.storageOpts.HISTORYDLG, function (data) {
            if (data){
                __list = mcService.StringToObj(data);

                $rootScope.$broadcast(window._messages_.historyDialogs.dListFillHistory, [getList()]);
            }
        });
    };

    this.addOrUpdateUser = function(userInfo, dt) {
        var uin = userInfo.UINFrom || userInfo.UIN;

        updateItem(uin, userInfo.DisplayName, dt);

        $rootScope.Storage.historyDialogs.save(mcConst.UserInfo.UIN, mcConst.storageOpts.HISTORYDLG, __list, function () {
            $rootScope.$broadcast(window._messages_.historyDialogs.dListAddOrModifyUser, [
                getItem(uin)
            ]);
        });
    };

    this.removeUser = function (uin) {
        if (__list.hasOwnProperty(uin)){
            delete __list[uin];

            $rootScope.Storage.historyDialogs.save(mcConst.UserInfo.UIN, mcConst.storageOpts.HISTORYDLG, __list);
        } else {
            console.error("History list has't UIN: " + uin);
        }

        $rootScope.$broadcast(window._messages_.historyDialogs.dListHistoryRemoveUser, [uin]);
    };

    this.getHistoryList = function () {
        return getList();
    }
}


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;
    };
}

function UsersCustomOptions($rootScope) {
    var uOptionList = {
        // 3: {
        //     autoReceive: true
        // }
    };
    var saveTimeout = null;

    // ------------------------------
    
    function __hasUin(uin) {
        return uOptionList.hasOwnProperty(uin);
    }
    
    function __save() {
        // uOptionList[uin] = data;

        $rootScope.Storage.dialogs.save(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.cUserOpt, JSON.stringify(uOptionList));
    }

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

    this.load = function () {
        $rootScope.Storage.dialogs.load(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.cUserOpt, function (data) {
            uOptionList = mcService.StringToObj(data) || {};
        });
    };

    this.saveAll = __save;

    this.getUser = function (uin) {
        return __hasUin(uin) ? uOptionList[uin] : {};
    };

    this.getOption = function (uin, name) {
        return __hasUin(uin) ? uOptionList[uin][name] : null;
    };

    this.setUser = function (uin, opt) {
        if (mcService.isObject(opt)){
            uOptionList[uin] = opt;
        } else {
            console.warn("UsersCustomOptions.setUser: can't save options, is not an object!");
        }
    };

    this.setUserOption = function (uin, name, val) {
        if (!__hasUin(uin)){
            uOptionList[uin] = {};
        }

        uOptionList[uin][name] = val;

        if (!saveTimeout){
            saveTimeout = setTimeout(function () {
                __save();
            }, 100);
        }
    }
}
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 mcCallManager($scope, $rootScope, mcSound) {
    "use strict";

    var currentUIN   = 0;
    var REJECTTIMER  = 60000;
    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 recorderRTC  = null;
    var disconnectTimer= 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 (mcService.checkRights(mcConst._CMD_.RS.QPrivAllowRecordMediaCalls)){
                cPanel.record = function (){
                    if (mediaCall.getCallType() === mcConst._CMD_.callType.VOICE) {
                        startAudioRecord(mediaCall.getCallStreams());
                    } else {
                        startRecord(mediaCall.getCallStreams());
                    }
                };
            }

            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 saveBlob(blob, name, type) {
        var reader = new FileReader();

        reader.onload = function() {
            if (reader.readyState === 2) {
                $scope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                    mcConst._CMD_.ce_chunk_media,
                    new Buffer(reader.result),
                    name,
                    type
                ]);

                if (!recorder){
                    setTimeout(function () {
                        $scope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                            mcConst._CMD_.ce_finish_media
                        ]);

                        // closeCall.apply(null, closeParams);
                    }, 100);
                }
            }
        };

        reader.readAsArrayBuffer(blob)
    }

    function startRecord(streams){
        var fileNameMy = mediaEvents.isMyCall
            ? mcConst.UserInfo.UIN + "-" + getUIN() + "-" + mcService.formatDate(new Date()) + ".webm"
            : getUIN() + "-" + mcConst.UserInfo.UIN + "-" + mcService.formatDate(new Date()) + ".webm";
        var fileTypeMy = mediaCall.useVideo ? "video" : mediaCall.shareScreen && isMyCall ? "video" : "audio";

        if (streams.length) {
            console.log("!Starting recording");

            recorderRTC = RecordRTC(streams, {
                // audio, video, canvas, gif
                type: 'video',

                // audio/webm
                // audio/webm;codecs=pcm
                // video/mp4
                // video/webm;codecs=vp9
                // video/webm;codecs=vp8
                // video/webm;codecs=h264
                // video/x-matroska;codecs=avc1
                // video/mpeg -- NOT supported by any browser, yet
                // audio/wav
                // audio/ogg  -- ONLY Firefox
                // demo: simple-demos/isTypeSupported.html
                mimeType: 'video/webm; codecs=vp9',

                // MediaStreamRecorder, StereoAudioRecorder, WebAssemblyRecorder
                // CanvasRecorder, GifRecorder, WhammyRecorder
                recorderType: MultiStreamRecorder,

                // disable logs
                disableLogs: true,

                // get intervals based blobs
                // value in milliseconds
                timeSlice: recordDuration,

                // requires timeSlice above
                // returns blob via callback function
                ondataavailable: function(data) {
                    saveBlob(data, fileNameMy, fileTypeMy);
                },

                // auto stop recording if camera stops
                checkForInactiveTracks: false,

                // requires timeSlice above
                onTimeStamp: function(timestamp) {},

                // both for audio and video tracks
                // bitsPerSecond: 96000,

                // only for audio track
                // ignored when codecs=pcm
                audioBitsPerSecond: 96000,

                // only for video track
                videoBitsPerSecond: 960000,

                // used by CanvasRecorder and WhammyRecorder
                // it is kind of a "frameRate"
                frameInterval: 19,

                // if you are recording multiple streams into single file
                // this helps you see what is being recorded
                previewStream: function(stream) {},

                // used by CanvasRecorder and WhammyRecorder
                // you can pass {width:640, height: 480} as well
                // video: HTMLVideoElement,

                // used by CanvasRecorder and WhammyRecorder
                /*canvas: {
                 width: 640,
                 height: 480
                 },*/

                video: {
                    height: 480,
                    width: 640,
                },

                // used by StereoAudioRecorder
                // the range 22050 to 96000.
                sampleRate: 96000,

                // used by StereoAudioRecorder
                // the range 22050 to 96000.
                // let us force 16khz recording:
                desiredSampRate: 16000,

                // used by StereoAudioRecorder
                // Legal values are (256, 512, 1024, 2048, 4096, 8192, 16384).
                bufferSize: 16384,

                // used by StereoAudioRecorder
                // 1 or 2
                numberOfAudioChannels: 2,

                // used by WebAssemblyRecorder
                frameRate: 19,

                // used by WebAssemblyRecorder
                bitrate: 960000,

                // used by MultiStreamRecorder - to access HTMLCanvasElement
                elementClass: 'multi-streams-mixer'
            });

            recorderRTC.startRecording();

            IItems.recordCall.className += "recordCallRed";
        } else {
            console.error("startRecord: all streams is empty");
        }
    }

    function stopRecord(){
        console.log("Stopping recording");

        var rec = recorderRTC;

        recorderRTC = null;

        if (rec.stopRecording) {
            rec.stopRecording();
        } else
        if (rec.stop){
            rec.stop();
        }

        rec = null;
    }

    function startAudioRecord(config, streams){
        var ctx        = new AudioContext();
        var dest       = ctx.createMediaStreamDestination();
        var fileNameMy = mediaEvents.isMyCall
            ? mcConst.UserInfo.UIN + "-" + getUIN() + "-" + mcService.formatDate(new Date()) + ".webm"
            : getUIN() + "-" + mcConst.UserInfo.UIN + "-" + mcService.formatDate(new Date()) + ".webm";

        streams.map(function (stream) {
            ctx.createMediaStreamSource(stream).connect(dest);
        });

        var recorder = new MediaRecorder(dest.stream/*stream.stream*//*, stream.settings.recorder*/);

        recorder.start(recordDuration);

        recorder.ondataavailable(function (e){
            saveBlob(e.data, fileNameMy, "audio");
        });
    }

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

    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, screenID){
        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,
                screenID,
            ]);
        }
    }

    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(skipCheck) {
        currentUIN = $rootScope.GetChatID();

        if (!$rootScope.isWebClient && skipCheck === "toolAudioCall"){
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_grant_access_media,
                mcConst.mediaType.mic
            ]);
        } else
        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>";
                    });

                    if (scrList.length === 1) {
                        _showStopCall();

                        // setScreenId(scrList[0].id);

                        myCall(getUIN(), mcConst._CMD_.callType.SHARING, scrList[0].id);
                    } else {
                        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, scrIdx);

                                        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) && (!nowCalling() || (nowCalling() && getUIN() == uin))) {
            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();
                } else

                // проверки просто открытого привата
                // юзер стал отлайн
                if ($rootScope.GetChatTypeID() !== $rootScope.chatAliases.Elisa && mcService.inArrayNoStrict($rootScope.GetChatID(), Statuses.Online) >= 0) {
                    !$rootScope.isWebClient && $scope.startShareCall.enable();
                    $scope.startVideoCall.enable();
                    $scope.startAudioCall.enable();
                } else
                // юзер стал офлайн
                if ($rootScope.GetChatTypeID() !== $rootScope.chatAliases.Elisa && mcService.inArrayNoStrict($rootScope.GetChatID(), Statuses.Offline) >= 0) {
                    !$rootScope.isWebClient && $scope.startShareCall.disable();
                    $scope.startVideoCall.disable();
                    $scope.startAudioCall.disable();
                }
            }
        }
    }

    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',
        grant_access_media          : 'grant_access_media'
    };

    $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.grant_access_media,   function(e, args){
        var info = args[0];

        if (info.type === mcConst.mediaType.mic && info.res) { // access to microphone granted
            _startVoiceCall(true);
        }
    });

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

    $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];

        obj.CallType = parseInt(obj.CallType);

        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];

        obj.CallType = parseInt(obj.CallType);
        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);
        }, REJECTTIMER);
    });
    $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 mcVoiceRecorder($rootScope) {
    "use strict";

    // ======= VOICE MESSAGES ===============
    // ======================================

    var Self                = this;
    var __gumStream         = null;  //stream from getUserMedia()
    var __recorder          = null;  //WebAudioRecorder object
    var __input             = null;  //MediaStreamAudioSourceNode  we'll be recording
    var __encodingType      = "ogg"; //holds selected encoding for resulting audio (file)
    var __encodeAfterRecord = true;  // when to encode
    var __audioContext      = window.AudioContext || window.webkitAudioContext; //new audio context to help us record

    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 __createDownloadLink(blob, encoding) {

        var url  = __URL.createObjectURL(blob);
        var au   = document.createElement('audio');
        // var li   = document.createElement('li');
        var link = document.createElement('a');

        // add controls to the <audio> element
        au.controls = true;
        au.src      = url;

        // link the a element to the blob
        link.href      = url;
        link.download  = new Date().toISOString() + '.'+encoding;
        link.innerHTML = link.download;

        // add the new audio and a elements to the li element
        // $scope.chatTextOutputW.getNode().appendChild(au);
        // $scope.chatTextOutputW.getNode().appendChild(link);

        // add the li element to the ordered list
        // recordingsList.appendChild(li);

        console.log( url );
    }

    function __log(e, data) {
        console.log(e + " " + (data || ''));
    }

    function __startRecording() {
        console.log("startRecording() called");

        // Simple constraints object, for more advanced features see
        // https://addpipe.com/blog/audio-constraints-getusermedia/

        var constraints = { audio: true, video:false };
        var __abort = false;

        var preStart = webix.alert({
            type : "alert-warning",
            ok   : mcLang(33),    // "33" :"Отмена",
            text : mcLang(677),   // "677":"Подготовка к записи",
            callback: function() {
                __abort = true;
            }
        });

        __getUserMedia(constraints, function(stream) {
            if (!__abort) {
                __log("getUserMedia() success...");

                webix.modalbox.hide(preStart);

                webix.confirm({
                    text  : mcService.Lang(676), // "676":"Говорите...",
                    ok    : mcService.Lang(19),  // "19" :"Отправить",
                    cancel: mcService.Lang(33),  // "33" :"Отмена",
                    width : 310,
                    callback: function(result){ //setting callback
                        __stopRecording(!result);
                    }
                });

                /*
                 create an audio context after getUserMedia is called
                 sampleRate might change after getUserMedia is called, like it does on macOS when recording through AirPods
                 the sampleRate defaults to the one set in your OS for your playback device

                 */
                __audioContext = new __AudioContext();

                //assign to gumStream for later use
                __gumStream = stream;

                /* use the stream */
                __input = __audioContext.createMediaStreamSource(stream);

                //stop the input from playing back through the speakers
                // __input.connect(__audioContext.destination);

                __recorder = new WebAudioRecorder(__input, {
                    workerDir       : MC_RESOURCE.TextSource.audioRecorder, // must end with slash
                    encoding        : __encodingType,
                    numChannels     : 1, //2 is the default, mp3 encoding supports only 2
                    onEncoderLoading: function(recorder, encoding) {
                        // show "loading encoder..." display
                        __log("Loading "+encoding+" encoder...");
                    },
                    onEncoderLoaded : function(recorder, encoding) {
                        // hide "loading encoder..." display
                        __log(encoding+" encoder loaded");
                    }
                });

                __recorder.onComplete = function(recorder, blob) {
                    __log("Encoding complete");

                    __createDownloadLink(blob,recorder.encoding);
                    // encodingTypeSelect.disabled = false;
                };

                __recorder.setOptions({
                    timeLimit        : 120,
                    encodeAfterRecord: __encodeAfterRecord,
                    ogg              : { quality: 0.3},
                    mp3              : { bitRate: 160}
                });

                //start the recording process
                __recorder.startRecording();

                __log("Recording started");
            } else {
                __log("Recording prepare aborted");
            }
        }, function(err) {
            //enable the record button if getUSerMedia() fails
            // recordButton.disabled = false;
            // stopButton.disabled   = true;
            __log("getUserMedia error ", err.message);
        });

        //disable the record button
        // recordButton.disabled = true;
        // stopButton.disabled   = false;
    }

    function __stopRecording(abort) {
        console.log("stopRecording() called");

        //stop microphone access
        __gumStream.getAudioTracks()[0].stop();

        //disable the stop button
        // stopButton.disabled = true;
        // recordButton.disabled = false;

        //tell the recorder to finish the recording (stop recording + encode the recorded audio)
        if (abort) {
            __recorder.cancelRecording();
        } else {
            __recorder.finishRecording();
        }

        __input     = null;
        __gumStream = null;

        __log('Recording stopped');
    }
    
    this.startRecording = __startRecording;
    this.stopRecording  = __stopRecording;
}

function mcEmojiList($rootScope) {
    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
    ];

    var popularSmileys = {};
    var saveTimer = null;

    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;
    };

    this.getPopularSmileys = function () {
        return popularSmileys;
    };

    this.incPopularSmileCount = function (code) {
        if (!popularSmileys.hasOwnProperty(code)) {
            popularSmileys[code] = 0;
        }
        
        popularSmileys[code] ++;

        if (!saveTimer){
            saveTimer = setTimeout(function (){
                saveTimer = null;
                
                $rootScope.Storage.popularSmiles.save(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.POPULARSMILES, JSON.stringify(popularSmileys));
            }, 2000);
        }
    };

    $rootScope.Storage.popularSmiles.load(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.POPULARSMILES, function (data) {
        popularSmileys = mcService.StringToObj(data);
    });
}










    function mcUsersSmallInfo($rootScope) {
    "use strict";

    // тут храниться краткая инфа про любых пользователей - приватов и конференций. Нельзя объединить с диалогами

    var __users             = {};
    var __updateRequestPool = [];
    var __updateInProgress  = false;
    var __confUins          = {};
    var __notifications     = [];

    var idbCon              = null;

    var templateNtf = function () {
        var list = [
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 0,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\">Вітаємо, <font color=\"#800000\" >%firstnameordisplayname%</font>!<br><br>Приємно, що ви встановили та користуєтеся MyChat.<br>Сподіваємося, корпоративний месенджер вирішить ваші задачі.<br><br>Будь ласка, задавайте питання на <a href=\"https://nsoft-s.com/forum/?%id%\">форумі підтримки</a>, відповідаємо швидко й по суті справи.</p><br><br><p align=\"right\"><a href=\"https://nsoft-s.com/ua/support.html?%id%\">Команда розробників MyChat</a></p>",
                    "RU": "<p align=\"center\">Здравствуйте, <font color=\"#800000\" >%firstnameordisplayname%</font>!<br><br>Приятно, что вы установили и пользуетесь MyChat.<br>Надеемся, корпоративный мессенджер решит ваши задачи.<br><br>Пожалуйста, задавайте вопросы на <a href=\"https://nsoft-s.com/forum/?%id%\">форуме поддержки</a>, отвечаем быстро и по делу.</p><br><br><p align=\"right\"><a href=\"https://nsoft-s.com/support.html?%id%\">Команда разработчиков MyChat</a></p>",
                    "EN": "<p align=\"center\">Hello, <font color=\"#800000\">%firstnameordisplayname%</font>!<br><br>We are glad that you installed and use MyChat.<br>We hope that the corporate messenger will help you with your tasks.<br><br>Please, ask your questions on the <a href=\"https://nsoft-s.com/mychatforum/?%id%\">support forum</a>, we answer quickly sticking to the case.</p><br><br><p align=\"right\"><a href=\"https://nsoft-s.com/en/support.html?%id%\">MyChat development team</a></p>"
                },
                "MsgTitleJSON": {
                    "RU": "Спрашивайте, мы отвечаем",
                    "UA": "Запитуйте, відповідаємо",
                    "EN": "MyChat support service"
                },
                "MsgType": 101,
                "OnlyForVer": "",
                "id": 1
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 0,
                "LockLang": "",
                "MsgBodyJSON": {},
                "MsgTitleJSON": {
                    "RU": "MyChat для Android",
                    "UA": "MyChat для Android",
                    "EN": "MyChat for Android"
                },
                "MsgType": 100,
                "OnlyForVer": "",
                "id": 2
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 1,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\">Вам потрібно підключити до корпоративного чату віддалені офіси чи окремих співробітників через інтернет?<br><br><a href=\"https://nsoft-s.com/ua/mychatarticles/1150-local-icq-nastroyka-mychat.html?%id%\">Розказуємо, як це зробити.</a></p>",
                    "RU": "<p align=\"center\">Вам нужно подключить к корпоративному чату удалённые офисы или отдельных сотрудников через интернет?<br><br><a href=\"https://nsoft-s.com/mychatarticles/939-local-icq-nastroyka-mychat.html?%id%\">Рассказываем, как это сделать.</a></p>",
                    "EN": "<p align=\"center\">Do you need to connect remote offices or employees via the internet to the corporate chat?<br><br><a href=\"https://nsoft-s.com/en/mychatarticles/1183-local-icq-mychat-settings.html?%id%\">Let us help you to do it.</a>"
                },
                "MsgTitleJSON": {
                    "RU": "MyChat через Интернет",
                    "UA": "MyChat через Інтернет",
                    "EN": "MyChat via Internet"
                },
                "MsgType": 101,
                "OnlyForVer": "",
                "id": 3
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 0,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\">MyChat вміє працювати на <a href=\"https://nsoft-s.com/ua/mcserverhelp/mychatclientmacos.htm?%id%\">макбуках</a> і в різних версіях <a href=\"https://nsoft-s.com/ua/mcserverhelp/mychatclientlinux.htm?%id%\">Linux.</a><br><br>Або у <a href=\"#webchat\">звичайному браузері</a>, навіть без установки, на будь-якому комп'ютері чи планшеті.</p>",
                    "RU": "<p align=\"center\">MyChat умеет работать на <a href=\"https://nsoft-s.com/mcserverhelp/mychatclientmacos.htm?%id%\">макбуках</a> и в разных версиях <a href=\"https://nsoft-s.com/mcserverhelp/mychatclientlinux.htm?%id%\">Linux.</a><br><br>Или в <a href=\"#webchat\">обычном браузере</a>, даже без установки, на любом компьютере или планшете.</p>",
                    "EN": "<p align=\"center\">MyChat can work on <a href=\"https://nsoft-s.com/en/mcserverhelp/mychatclientmacos.htm?%id%\">MacBook</a> and various versions of <a href=\"https://nsoft-s.com/en/mcserverhelp/mychatclientlinux.htm?%id%\">Linux.</a><br><br>Or in a <a href=\"#webchat\">regular browser</a> without installation, on any computer or tablet.</p>"
                },
                "MsgTitleJSON": {
                    "RU": "Linux, macOS и WEB-браузер",
                    "UA": "Linux, macOS та WEB-браузер",
                    "EN": "Linux, macOS or WEB-browser"
                },
                "MsgType": 102,
                "OnlyForVer": "",
                "id": 4
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 1,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\">У вашій компанії є сайт? А онлайн-чат підтримки на ньому?<br><br><a href=\"https://nsoft-s.com/ua/mychatarticles/1215-yak-vstanovyty-chat-na-sait.html?%id%\">Встановіть чат на сайт</a> для ваших клієнтів і отримуйте повідомлення із сайту прямо в свій месенджер MyChat. Безкоштовно та надійно.</p>",
                    "RU": "<p align=\"center\">У вашей компании есть сайт? А онлайн-чат поддержки на нём?<br><br><a href=\"https://nsoft-s.com/mychatarticles/1176-kak-ustanovit-chat-na-sayt.html?%id%\">Установите чат на сайт</a> для ваших клиентов и получайте сообщения с сайта прямо в свой мессенджер MyChat. Бесплатно и надёжно.</p>",
                    "EN": "<p align=\"center\">Does your company have a website? What about a support chat on it?<br><br><a href=\"https://nsoft-s.com/en/mychatarticles/1181-how-to-install-free-website-chat.html?%id%\">Install the chat on a website</a> for your customers and receive messages from the website directly in your MyChat messenger. Reliably and for free.</p>"
                },
                "MsgTitleJSON": {
                    "RU": "Чат поддержки на сайт",
                    "UA": "Чат підтримки на сайт",
                    "EN": "Website support chat"
                },
                "MsgType": 101,
                "OnlyForVer": "",
                "id": 5
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 1,
                "LockLang": "RU,UA",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\">Вітаємо, ви вже більше місяця використовуєте месенджер MyChat.<br><br>Скоріше за все, у вас накопичилися питання про функції та можливості програми, котрі не вийшло вирішити самостійно.<br><br>Спілкуємося голосом будь-яким зручним вам месенджером або по телефону, підключаємося віддалено через AnyDesk або TeamViewer.<br><br>Консультуємо, навчаємо, допомагаємо налаштувати, розповідаємо, що й як.<br><br><br><a href=\"https://nsoft-s.com/ua/mychat-premium-support.html?%id%\">Платна технічна підтримка</a>, для комерційних та free-версій.</p>",
                    "RU": "<p align=\"center\">Поздравляем, вы уже больше месяца используете мессенджер MyChat.<br><br>Наверняка у вас накопились вопросы по функциям и возможностям, которые не получилось решить самостоятельно.<br><br>Созваниваемся любым удобным вам мессенджером или по телефону, подключаемся удалённо через AnyDesk или TeamViewer.<br><br>Консультируем, обучаем, помогаем настроить, рассказываем, что и как.<br><br><br><a href=\"https://nsoft-s.com/mychat-premium-support.html?%id%\">Платная техническая поддержка</a>, для коммерческих и free-версий.</p>",
                    "EN": ""
                },
                "MsgTitleJSON": {
                    "RU": "Поможем настроить MyChat",
                    "UA": "Допоможемо налаштувати MyChat",
                    "EN": ""
                },
                "MsgType": 107,
                "OnlyForVer": "",
                "id": 6
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 1,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\">Якщо у вашій компанії працює один або декілька доменів, то краще використовувати інтеграцію MyChat в Active Directory.<br><br>Це зручно з трьох причин:<br><br></p><p align=\"left\">1. Швидке розгортування MyChat Client через GPO.</p><br><p align=\"left\">2. Прозора авторизація, користувачам не потрібно вводити логін і пароль та вказувати адресу сервера для підключення.</p><br><p align=\"left\">3. Вся інформація з домену (структура компанії, ПІБ, фото, адреса та телефони) імпортується у ваш MyChat Server і автоматично створюється загальний деревовидний список контактів користувачів корпоративного чату.<br><br></p><p align=\"center\">Почитайте, <a href=\"https://nsoft-s.com/ua/mcserverhelp/howtoinstallactivedirectory.htm?%id%\">як правильно це налаштувати</a>.<br><br>Як завжди, чекаємо ваші питання на <a href=\"https://nsoft-s.com/forum?%id%\">форумі підтримки</a>.</p>",
                    "RU": "<p align=\"center\">Если в вашей компании работает один или несколько доменов, то лучше использовать интеграцию MyChat в Active Directory.<br><br>Это удобно по трём причинам:<br><br></p><p align=\"left\">1. Быстрое развёртывание MyChat Client через GPO.</p><br><p align=\"left\">2. Прозрачная авторизация, пользователям не надо вводить логин и пароль и указывать адрес сервера для подключения.</p><br><p align=\"left\">3. Вся информация из домена (структура компании, ФИО, фото, адреса и телефоны) импортируется в ваш MyChat Server и автоматически создаётся общий древовидный список контактов пользователей корпоративного чата.​​​​<br><br></p><p align=\"center\">Почитайте, <a href=\"https://nsoft-s.com/mcserverhelp/howtoinstallactivedirectory.htm?%id%\">как это правильно настроить</a>.<br><br>Как обычно, ждём ваших вопросов на <a href=\"https://nsoft-s.com/forum?%id%\">форуме поддержки</a>.</p>",
                    "EN": "<p align=\"center\">If your company has one or more domains, it is better to use MyChat in Active Directory.<br><br>It is convenient for three reasons:<br><br></p><p align=\"left\">1. Quick deployment of MyChat Client via GPO.</p><br><p align=\"left\">2. Transparent authorization. Users do not need to enter login, password, and specify a server address for connection.</p><br><p align=\"left\">3. All the information from a domain (company structure, full name, photo, address and phone numbers) is imported to your MyChat Server, and then a common tree-like contact list of corporate chat users is automatically created.<br><br></p><p align=\"center\">Read <a href=\"https://nsoft-s.com/en/mcserverhelp/howtoinstallactivedirectory.htm?%id%\">how to configure it correctly</a>.<br><br>As usual, we are waiting for your questions on the <a href=\"https://nsoft-s.com/mychatforum/?%id%\">support forum</a>.</p>"
                },
                "MsgTitleJSON": {
                    "RU": "MyChat в Active Directory",
                    "UA": "MyChat в Active Directory",
                    "EN": "MyChat in Active Directory"
                },
                "MsgType": 101,
                "OnlyForVer": "",
                "id": 7
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 0,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\">MyChat Client вміє працювати з флешки, без встановлення на комп'ютер.<br><br><a href=\"https://nsoft-s.com/ua/mcserverhelp/howtoinstallmychatclientportable.htm?%id%\">Завантажити portable-версію</a>.</p>",
                    "RU": "<p align=\"center\">MyChat Client умеет работать с флешки, без установки на компьютер.<br><br><a href=\"https://nsoft-s.com/mcserverhelp/howtoinstallmychatclientportable.htm?%id%\">Скачать portable-версию</a>.</p>",
                    "EN": "<p align=\"center\">MyChat Client can work from a flash drive without installation on a computer.<br><br><a href=\"https://nsoft-s.com/en/mcserverhelp/howtoinstallmychatclientportable.htm?%id%\">Download the portable version</a>.</p>"
                },
                "MsgTitleJSON": {
                    "RU": "Portable-версия MyChat",
                    "UA": "Portable-версія MyChat",
                    "EN": "MyChat Portable"
                },
                "MsgType": 101,
                "OnlyForVer": "",
                "id": 8
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 1,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\"><a href=\"https://nsoft-s.com/ua/aboutmychat/mychat-distrib-maker.html?%id%\">Спробуйте сервіс</a> <b>MyChat Distrib Maker</b>, щоб зробити інсталятор MyChat Client з готовими налаштуваннями, своєю адресою сервера та набором плагінів.<br><br>Декілька кліків мишкою — й ви отримуєте на виході готовий EXE-установник, максимально \"заточений\" під потреби вашої компанії.</p>",
                    "RU": "<p align=\"center\"><a href=\"https://nsoft-s.com/aboutmychat/mychat-distrib-maker.html?%id%\">Попробуйте сервис</a> <b>MyChat Distrib Maker</b>, чтобы сделать инсталлятор MyChat Client с готовыми настройками, своим адресом сервера и набором плагинов.<br><br>Несколько кликов мышкой — и вы получаете на выходе готовый EXE-установщик, максимально \"заточенный\" под нужды вашей компании.</p>",
                    "EN": "<p align=\"center\"><a href=\"https://nsoft-s.com/en/aboutmychat/mychat-distrib-maker.html?%id%\">Try the service</a> <b>MyChat Distrib Maker</b> to make MyChat Client installer with ready settings, own server address, and a set of plugins.<br><br>A couple of clicks and you get a ready EXE installer made exactly for your company needs.</p>"
                },
                "MsgTitleJSON": {
                    "RU": "Свой дистрибутив MyChat",
                    "UA": "Свій дистрибутив MyChat",
                    "EN": "Your MyChat custom setup"
                },
                "MsgType": 107,
                "OnlyForVer": "",
                "id": 9
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 0,
                "LockLang": "RU,UA",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\"><a href=\"https://nsoft-s.com/ua/aboutmychat/bot-viktorina.html?%id%\">Робот-вікторина для MyChat</a><br><br>Більше <font color=\"#800000\">120 тисяч</font> запитань, рейтинги найкращих гравців, бонуси за серії правильних відповідей, підказки, гнучкі налаштування.</p>",
                    "RU": "<p align=\"center\"><a href=\"https://nsoft-s.com/aboutmychat/robot-viktorina.html?%id%\">Робот-викторина для MyChat</a><br><br>Больше <font color=\"#800000\">120 тысяч</font> вопросов, рейтинги лучших игроков, бонусы за серии правильных ответов, подсказки, гибкие настройки.</p>",
                    "EN": ""
                },
                "MsgTitleJSON": {
                    "RU": "Викторина в чате",
                    "UA": "Вікторина в чаті",
                    "EN": ""
                },
                "MsgType": 108,
                "OnlyForVer": "",
                "id": 10
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 0,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\"><font color=\"#800000\" >%firstnameordisplayname%</font>, вам подобається MyChat?<br><br><a href=\"#tellaboutmychat\">Розкажіть про це</a> своїм друзям та колегам у соцмережах, нам буде дуже приємно.<br><br>А якщо є за що посварити — не зтримуйтеся, чекаємо вас на <a href=\"https://nsoft-s.com/forum?%id%\">форумі підтримки</a>. Вислухаємо й допоможемо.</p>",
                    "RU": "<p align=\"center\"><font color=\"#800000\" >%firstnameordisplayname%</font>, вам нравится MyChat?<br><br><a href=\"#tellaboutmychat\">Расскажите про это</a> своим друзьям и коллегам в соцсетях, нам будет очень приятно.<br><br>А если есть за что поругать — не сдерживайте себя, ждём вас на <a href=\"https://nsoft-s.com/forum?%id%\">форуме поддержки</a>. Выслушаем и поможем.</p>",
                    "EN": "<p align=\"center\"><font color=\"#800000\" >%firstnameordisplayname%</font>, do you like MyChat?<br><br><a href=\"#tellaboutmychat\">Tell about your experience</a> your friends and colleagues in social media, and we would be pleased.<br><br>And if you have something to scold us for — do not hold it to yourself, we are waiting for you on the <a href=\"https://nsoft-s.com/mychatforum?%id%\">support forum</a>. We will listen and help you.</p>"
                },
                "MsgTitleJSON": {
                    "RU": "MyChat в соцсетях",
                    "UA": "MyChat у соцмережах",
                    "EN": "MyChat in social networks"
                },
                "MsgType": 101,
                "OnlyForVer": "",
                "id": 11
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 0,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\"><b>Kanban</b> — це просте та ефективне управління задачами та проектами.<br><br>Вбудований у ваш MyChat, працює в браузері.<br><br><a href=\"#kanban\">Відкрити</a></p>",
                    "RU": "<p align=\"center\"><b>Kanban</b> — это простое и эффективное управление задачами и проектами.<br><br>Встроен в ваш MyChat, работает в браузере.<br><br><a href=\"#kanban\">Открыть</a></p>",
                    "EN": "<p align=\"center\"><b>Kanban</b> is a simple and effective management of tasks and projects.<br><br>It is built-in to your MyChat and work in a browser.<br><br><a href=\"#kanban\">Open</a></p>"
                },
                "MsgTitleJSON": {
                    "RU": "Управление проектами Kanban",
                    "UA": "Управління проектами Kanban",
                    "EN": "Kanban project management "
                },
                "MsgType": 101,
                "OnlyForVer": "",
                "id": 12
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 0,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\">Публікуйте у <a href=\"#forum\">вбудованому форумі</a> тексти та файли.<br><br>Все структуровано й розкладено \"по поличкам\" для швидкого доступу.<br><br>Працює у браузері.</p>",
                    "RU": "<p align=\"center\">Публикуйте во <a href=\"#forum\">встроенном форуме</a> тексты и файлы.<br><br>Всё структурировано и разложено \"по полочкам\" для быстрого доступа.<br><br>Работает в браузере.</p>",
                    "EN": "<p align=\"center\">Publish texts and files on the <a href=\"#forum\">internal forum</a>.<br><br>Everything is structured and organized for quick access.<br><br>The forum works in a browser.</p>"
                },
                "MsgTitleJSON": {
                    "RU": "Встроенный форум",
                    "UA": "Вбудований форум",
                    "EN": "Internal forum"
                },
                "MsgType": 101,
                "OnlyForVer": "",
                "id": 13
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 1,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\"><font color=\"#800000\" >%firstnameordisplayname%</font>, ви вмієте програмувати?<br><br>В MyChat Server можна писати скрипти.<br>Великий набір функцій, зручний редактор, <a href=\"https://nsoft-s.com/ua/mcserverhelp/scripts.htm?%id%\">гарна документація й багато прикладів</a>.<br><br>Можна зробити багато корисного, чого бракує в стандартному функціоналі.<br><br><a href=\"#scripteditor\">Написати скрипт</a></p>",
                    "RU": "<p align=\"center\"><font color=\"#800000\" >%firstnameordisplayname%</font>, вы умеете программировать?<br><br>В MyChat Server можно писать скрипты.<br>Большой набор функций, удобный редактор, <a href=\"https://nsoft-s.com/mcserverhelp/scripts.htm?%id%\">хорошая документация и много примеров</a>.<br><br>Можно сделать много такого, чего нет в стандартном функционале.<br><br><a href=\"#scripteditor\">Написать скрипт</a></p>",
                    "EN": "<p align=\"center\"><font color=\"#800000\" >%firstnameordisplayname%</font>, can you program?<br><br>MyChat Server has an option for writing scripts.<br>A big set of functions, convenient editor, <a href=\"https://nsoft-s.com/en/mcserverhelp/scripts.htm?%id%\">good documentation, and a lot of examples</a>.<br><br>It is the opportunity to do something that is not included in default features.<br><br><a href=\"#scripteditor\">Write a script</a></p>"
                },
                "MsgTitleJSON": {
                    "RU": "Серверные скрипты",
                    "UA": "Серверні скрипти",
                    "EN": "Server scripts"
                },
                "MsgType": 101,
                "OnlyForVer": "",
                "id": 14
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 1,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\">Якщо ви не вмієте писати скрипти для MyChat<br>чи просто немає часу розбиратися в різних тонкощах,<br>а рішення треба вже зараз — не біда.<br><br>Пишемо скрипти на замовлення. Швидко та якісно.<br><br><a href=\"https://nsoft-s.com/ua/order-custom-script.html?%id%\">Замовити</a></p>",
                    "RU": "<p align=\"center\">Если вы не умеете писать скрипты для MyChat<br>или просто нет времени разбираться в разных тонкостях,<br>а решение надо \"здесь и сейчас\" — не беда.<br><br>Пишем скрипты на заказ. Быстро и качественно.<br><br><a href=\"https://nsoft-s.com/order-custom-script.html?%id%\">Заказать</a></p>",
                    "EN": "<p align=\"center\">If you can't write scripts for MyChat<br>or do not have time to understand how it works,<br>but you need a solution right here and right now — it's not a problem.<br><br>We write scripts to order. Quick and qualitatively.<br><br><a href=\"https://nsoft-s.com/en/order-custom-script.html?%id%\">Order</a></p>"
                },
                "MsgTitleJSON": {
                    "RU": "Пишем скрипты на заказ",
                    "UA": "Пишемо скрипти на замовлення",
                    "EN": "Writing scripts to order"
                },
                "MsgType": 101,
                "OnlyForVer": "",
                "id": 15
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 0,
                "LockLang": "",
                "MsgBodyJSON": {},
                "MsgTitleJSON": {
                    "RU": "MyChat для iPhone/iPad",
                    "UA": "MyChat для iPhone/iPad",
                    "EN": "MyChat for iPhone/iPad"
                },
                "MsgType": 109,
                "OnlyForVer": "",
                "id": 16
            },
            {
                "ForApp": "",
                "ForLicenseType": 2,
                "ForUsers": 0,
                "LockLang": "",
                "MsgBodyJSON": {
                    "UA": "<p align=\"center\">Строк використання <a href=\"https://nsoft-s.com/forum/viewtopic.php?f=39&t=3581\">безкоштовної версії</a> MyChat закінчився.<br><br>Будь ласка, <a href=\"https://nsoft-s.com/ua/buymychat.html?path=n1.r2.c4&count=2\">купіть ліцензію</a>.</p>",
                    "RU": "<p align=\"center\">Срок использования <a href=\"https://nsoft-s.com/forum/viewtopic.php?f=39&t=3581\">бесплатной версии</a> MyChat MyChat закончился.<br><br>Пожалуйста, <a href=\"https://nsoft-s.com/buymychat.html?path=n1.r2.c4&count=2\">приобретите лицензию</a>.</p>",
                    "EN": "<p align=\"center\">Your MyChat <a href=\"https://nsoft-s.com/mychatforum/viewtopic.php?f=39&t=4035\">free version</a> has expired.<br><br>Please, <a href=\"https://nsoft-s.com/en/buymychat.html?path=n1.r2.c4&count=2\">purchase the license</a>.</p>"
                },
                "MsgTitleJSON": {
                    "RU": "Требуется лицензия",
                    "UA": "Необхідна ліцензія",
                    "EN": "Free version has expired"
                },
                "MsgType": 114,
                "OnlyForVer": "",
                "id": 17
            }
        ];

        list.forEach(function (item) {
            item.uin_owner = mcConst.UserInfo.UIN;
        });


        return list;
    }

    var tblUsers    = {
        name   : "Users",
        columns: {
            id          : { autoIncrement: true },
            UIN         : { notNull: true, dataType: JsStore.DATA_TYPE.Number, primaryKey: true },
            DisplayName : { notNull: true, dataType: JsStore.DATA_TYPE.String },
            Avatar      : { notNull: true, dataType: JsStore.DATA_TYPE.Number },
            Sex         : { notNull: true, dataType: JsStore.DATA_TYPE.Number },
            ChangeID    : { notNull: true, dataType: JsStore.DATA_TYPE.Number },
            TeamLead    : {                dataType: JsStore.DATA_TYPE.Boolean },
        }
    };
    var tblConfUINs = {
        name : 'ConfUINs',
        columns : {
            UID   : { notNull: true, dataType: JsStore.DATA_TYPE.Number, primaryKey: true},
            CRC32 : { notNull: true, dataType: JsStore.DATA_TYPE.Number},
            UINS  : { notNull: true, dataType: JsStore.DATA_TYPE.String}
        }
    };
    var tblAvatars  = {
        name: 'Avatars',
        columns: {
            CRC32 : { notNull: true, dataType: JsStore.DATA_TYPE.Number, primaryKey: true },
            UIN   : { notNull: true, dataType: JsStore.DATA_TYPE.Number, primaryKey: true },
            Data  : {                dataType: JsStore.DATA_TYPE.String },
            Width : {                dataType: JsStore.DATA_TYPE.Number },
            Height: {                dataType: JsStore.DATA_TYPE.Number },
        }
    };
    var tblNotifications = {
        name: 'Notifications',
        columns: {
            // "serv_id"     : { notNull: true, dataType: JsStore.DATA_TYPE.Number,  primaryKey: true}, //INTEGER NOT NULL,
            "uin_owner"   : { notNull: true, dataType: JsStore.DATA_TYPE.Number/*,  primaryKey: true*/}, // INTEGER NOT NULL,
            "id"          : { notNull: true, dataType: JsStore.DATA_TYPE.Number,  primaryKey: true}, // INTEGER NOT NULL,
            "Actual"      : { notNull: true, dataType: JsStore.DATA_TYPE.Boolean, default: true }, // BOOLEAN DEFAULT TRUE,
            "dtCreatedUTC": {                dataType: JsStore.DATA_TYPE.DateTime }, // DATETIME,
            "dtStartUTC"  : {                dataType: JsStore.DATA_TYPE.DateTime }, // DATETIME,
            "dtValidUntilUTC": {             dataType: JsStore.DATA_TYPE.DateTime }, // DATETIME,
            "MsgType"     : { notNull: true, dataType: JsStore.DATA_TYPE.Number, default: 1 }, // INTEGER NOT NULL DEFAULT 1,
            "OnlyForVer"  : {                dataType: JsStore.DATA_TYPE.String }, // VARCHAR,
            "LockLang"    : {                dataType: JsStore.DATA_TYPE.String }, // VARCHAR,
            "ForLicenseType":{notNull: true, dataType: JsStore.DATA_TYPE.Number, default: 0 }, // INTEGER NOT NULL DEFAULT 0
            "ForUsers"    : { notNull: true, dataType: JsStore.DATA_TYPE.Number, default: 0 }, // INTEGER NOT NULL DEFAULT 0
            "ForApp"      : {                dataType: JsStore.DATA_TYPE.String }, // VARCHAR,
            "MsgTitleJSON": {                dataType: JsStore.DATA_TYPE.Object }, // VARCHAR,
            "MsgBodyJSON" : {                dataType: JsStore.DATA_TYPE.Object }, // VARCHAR,
            "Viewed"      : { notNull: true, dataType: JsStore.DATA_TYPE.Boolean, default: false } // BOOLEAN NOT NULL DEFAULT FALSE);
        }
    }
    
    function _getMaxID() {
        var maxID = 0;

        Object.keys(__users).forEach(function(user){
            if (__users[user].ChangeID > maxID) {
                maxID = __users[user].ChangeID;
            }
        });
        
        return maxID;
    }

    function _setInfo(info){
        if (info && info.DisplayName){
            __users[info.UIN] = info;
        } else

        if (info.UIN && info.ChangeID == -1){
            __users[info.UIN] = info;
            __users[info.UIN].DisplayName = info.UIN + " deleted";
        }
    }

    function _getInfo(uin) { // changeId
        if (uin !== void(0)) {
            if (!__users[uin] || !__users[uin].DisplayName){
                console.log(uin + ":" + __users[uin]);
            }

            if (uin === 0 && __users[uin] && !__users[uin].DisplayName) {
                
            }

            return mcService.Marge({DisplayName: "noNameUser", UIN: uin}, __users[uin]);
            // return structuredClone(__users[uin]);
        } else {
            return null;
        }
    }

    function _getUsersInfoByUins(uins, cb, invite) {
        if (mcService.isNumber(uins)){
            uins = uins.toString();
        }
        
        if (uins){
            /*!!!!!!!!!!!!!console.log("_getUsersInfoByUins: " + window.performance.now());/*!!!!!!!!!!!!!*/

            if (invite) {
                if (uins.split(',').length === 1 && __users[uins]){
                    cb && cb(_getInfo(uins));
                } else {
                    $rootScope.$broadcast('SendCMDToServer' , [
                        mcConst._CMD_.cs_get_users_changeid_data,
                        mcConst.SessionID,

                        uins,

                        function (resList) {
                            _updateUsersList(resList);

                            var data = "";

                            if (uins !== "" && (mcService.isNumber(uins) || uins.indexOf(',') === -1)) {
                                data = _getInfo(uins)
                            }

                            cb && cb(data);
                        }
                    ]);
                }
            } else {
                uins = uins.split(',');

                cb && cb(uins.map(function (uin){
                    return __users[uin];
                }));
            }
        }
    }

    function _updateUsersList(data, toBase) {
        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]);
            });
        }

        if (toBase && idbCon && data){
            idbCon.insert({
                into: tblUsers.name,
                values: mcService.isArray(data) && data.length ? data : Object.keys(data).map(function (user) {
                    return data[user];
                }),
                upsert: true
            }).catch(function(err) {
                console.log(err.message);
            });
        }
    }

    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, id) {
                if (mcService.isString(user) || mcService.isNumber(user)){
                    user = {
                        UIN: user
                    };
                    users[id] = user;
                }
                
                var localUser = __users[user.UIN];

                if (!localUser || (localUser && localUser.ChangeID != user.ChangeID) ) {
                    updateUsers.push(user.UIN);
                } else
                if (localUser && localUser.ChangeID == user.ChangeID) {
                    users[id] = mcService.Marge(user, localUser);
                }
            });

            var uins = updateUsers.join(',');

            if (uins) {
                $rootScope.$broadcast('SendCMDToServer' , [
                    mcConst._CMD_.cs_get_users_changeid_data,
                    mcConst.SessionID,

                    uins,

                    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(); // где бля список?
        }
    }

    function _getNotificationList(cb) {
        if (!__notifications.length){
            idbCon.insert({
                into  : tblNotifications.name,
                values: templateNtf(),
                // upsert: true
            }).catch(function(err) {
                console.log(err.message);
            });

            __notifications = templateNtf();
        }

        var res  = mcService.Marge([], __notifications);

        cb && cb(res)
        
        return res;
    }

    function _load(cb) {
        Promise.all([
            idbCon
            // All users information
            .select({
                from: tblUsers.name
            })
            .then(function(users) {
                _updateUsersList(users)

                console.log('Users loaded: ' + users.length);


            }),

            // Conference list users
            idbCon.select({
                from: tblConfUINs.name
            })
            .then(function (uins){
                __confUins = uins;
            }),

            // Notifications list
            idbCon.select({
                from: tblNotifications.name,
                where: {
                    uin_owner : mcConst.UserInfo.UIN
                }
            })
            .then(function (data){
                __notifications = data;
            })
        ])
        .then(function (){
            cb && cb();
        })

        .catch(function(err) {
            console.log(err.message);
        });
    }

    async function _initDB(servID, cb) {
        var dataBase = {
            name    : servID,
            tables  : [tblUsers, tblConfUINs, tblAvatars, tblNotifications],
            version : 5
        };
        
        idbCon = new JsStore.Connection(new Worker($rootScope.isWebClient ? "/mctools/other/jsstore.worker.min.js" : "modules/mctools/other/jsstore.worker.min.js"));
        idbCon.dbName = servID;

        var isDbCreated = await idbCon.initDb(dataBase);

        if (isDbCreated) {
            console.log('db created');
        } else {
            console.log('db opened');
        }

        $rootScope.dbConnect = idbCon;

        _load(cb);
    }

    function _getConfUins(uid, cb) {
        idbCon.select({
            from : tblConfUINs.name,
            where: {
                UID: uid
            }
        }).then(function (list) {
            // console.log("ok uid: " + uid);
            cb && cb(list);
        }).catch(function(err) {
            console.error(err.message || err);
            err && err.stack && console.error(err.stack);
            console.error("err uid: " + uid);
        });
    }

    function _setConfUins(uid, crc32, uins) {
        idbCon.insert({
            into  : tblConfUINs.name,
            values: [{
                UID  : parseInt(uid),
                CRC32: crc32,
                UINS : uins
            }],
            upsert: true
        }).catch(function(err) {
            console.log(err.message);
        });
    }

    function _getUserAvatarCRC(uins){
        var res = [];

        if (uins) (mcService.isArray(uins) ? uins : uins.toString().split(',')).forEach(function (_uin) {
            var user = _getInfo(_uin);

            if (user){
                res.push(user.Avatar);
            }
        });

        return res;
    }

    var msg = window._messages_.usersSmallInfo = {
        getUserSmallInfo        : "getUserSmallInfo",
        setUserSmallInfo        : "setUserSmallInfo",
        updateUsersSmallInfo    : "updateUsersSmallInfo",
        getInfoByUserUINs       : "getInfoByUserUINs",
        getAllUsersInfo         : "getAllUsersInfo",
        setAllUsersInfo         : "setAllUsersInfo",
        getMaxUserID            : "getMaxUserID",
        initDB                  : "initDB",
        getConfUins             : "getConfUins",
        setConfUins             : "setConfUins",
        reloadBase              : "reloadBase",
        getUsersAvatarCRC       : "getUserAvatarCRC",
        getNotifications        : "getNotifications"
    };

    $rootScope.$on(msg.initDB, function (e, args) {
        _initDB.apply(null, args);
    });

    $rootScope.$on(msg.reloadBase, function (e, args) {
        _load.apply(null, args);
    });

    $rootScope.$on(msg.getConfUins, function (e, args) {
        _getConfUins.apply(null, args);
    });

    $rootScope.$on(msg.setConfUins, function (e, args) {
        _setConfUins.apply(null, args);
    });

    $rootScope.$on(msg.getInfoByUserUINs, function (e, args) {
        _getUsersInfoByUins.apply(null, args);
    });

    $rootScope.$on(msg.getUsersAvatarCRC, function (e, args) {
        return _getUserAvatarCRC.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.getMaxUserID, function () {
        return _getMaxID();
    });

    $rootScope.$on(msg.updateUsersSmallInfo, function (e, args) {
        _getOrUpdateUsers.apply(null, args);
    });

    $rootScope.$on(msg.getAllUsersInfo, function () {
        return mcService.Marge({}, __users);
        // return structuredClone(__users);
    });

    $rootScope.$on(msg.setAllUsersInfo, function (e, args) {
        _updateUsersList.apply(null, args);
    });

    $rootScope.$on(msg.getNotifications, function (e, args) {
        return _getNotificationList.apply(null , args);
    });
}
// 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 __treeContacts($scope, title){
    "use strict";

    var __view = $scope.__view || $$($scope.Name);

    if (!__view){
        var cFilter = new mcService.FilterFiled({
            listName     : $scope.Name,
            id           : "treeContacts" + $scope.Name,
            css          : "noBorderAll",
            noClearBtn   : true,
            placeholder  : mcLang(48), // "48" :"Поиск...",
            filterParams : ['DisplayName']
        });

        var contacts = { id: $scope.Name + "_parent", rows: [
            { cols: [
                { hidden: !title, view: "label", label: title, align: 'center'},

                { view:"toggle", type:"iconButton", id: "pin" + $scope.Name, offIcon: "nezamok", onIcon: "lock", width: 30, css: "btnPaddingToggle4", paddingX: 3, on:{
                    onItemClick: $scope.pinTool
                }, tooltip: mcLang(511)} //  "511":"Закрепить/открепить",
            ]},

            cFilter,

            {
                view          : "treetable",
                resizeColumn  : true,
                id            : $scope.Name,
                select        : "row",
                header        : false,
                navigation    : false,
                scroll        : "y",
                css           : "noBorderAll noWhiteSpace scrollMenu noBG noPaddingCell",
                fixedRowHeight: true,//false,
                rowLineHeight : 25,
                rowHeight     : 25,
                scheme        : {
                    $sort: function (a, b){
                        if (a.folder && b.folder){
                            return mcService.sortAB(a.DisplayName, b.DisplayName);
                        } else
                        if (!a.folder && !b.folder){
                            return mcConst.ClientSettings.GeneralContactsSortStyle == 1
                                ? mcService.sortAB(a.DisplayName, b.DisplayName)
                                : mcService.sortAB(a.UIN, b.UIN);
                        } else {
                            return 0;
                        }
                    },
                },
                columns       : [{
                    id      : "DisplayName",
                    template: $scope._template || function (obj, common) {
                        return mcService.myReplaceFormated(
                            common.space(obj) +
                            mcService.tree_icon(obj) +
                            "<span class='webix_tree_item ' style='cursor: pointer;' title='#{title}'>" +
                                "<span class='webix_icon fa-user smaller #{isUser} #{stat}' style='color: #{color}'></span>" +
                                "<span class='#{brCSS}'>#{name}</span>" +
                            "</span>",
                            {
                                color  : mcService.getColorForStatus(obj.onlineState),
                                isUser : obj.folder ? "hidden" : "",
                                isFolder:obj.folder ? "" : "hidden",
                                brCSS  : obj.folder ? "treeBranchContacts webix_tree_item green" : "",
                                stat   : mcConst.storageOpts.STATUSICO + obj.UIN,
                                name   : obj.DisplayName,
                                title  : obj.folder ? "" : obj.DisplayName + ", UIN: " + obj.UIN
                            }
                        );
                    },
                    fillspace: true
                }/*, {
                    id    : "State",
                    header: [{content:"numberFilter"}],
                    hidden: true
                }*/],
                on: {
                    onItemClick: function (item) {
                        var id = item.row;

                        if (this.isBranch(id) || this.getItem(id).folder) {
                            if (this.isBranchOpen(id)){
                                this.close(id);
                            } else {
                                this.open(id);
                            }

                            // $scope.changeState && $scope.changeState();
                        } else {
                            $scope.openPrivate(item);
                        }
                    },
                    onAfterClose: function (){
                        $scope.changeState && $scope.changeState();
                    },
                    onAfterOpen: function (){
                        $scope.changeState && $scope.changeState();
                    }
                }
            }
        ]};

        $$($scope.container).addView(contacts);

        __view = $$($scope.Name);
        __view.parentContainer = $$($scope.Name + "_parent");
        $scope.__view = __view;

        $scope.pinBtn = $$("pin" + $scope.Name);

        mcService.disableContextMenu(__view);
    } else {
        __view.getParentView().show();

        $scope.pinBtn.setValue(false);
    }

    return __view;
}
function initLogin($scope){
    "use strict";

    var labelWidth = 140;
    var login = null;

    if ($$($scope.Name)) $$($scope.Name).destructor();

    var srvPwdPopup = webix.ui({
        view  : "popup",
        width : 300,
        height: 150,
        body  : { autoheight: true, css:"wrap", borderless: true,
            template: mcService.insertIco("fa-info-circle") + mcService.Lang(64)} // "64" : "Общий пароль, который проверяется перед авторизацией пользователя.<br><br>Устанавливается на сервере в разделе \"Настройки\" -&gt; \"Сетевые настройки\"",
    });

    var loginPopup = webix.ui({
        view  : "popup",
        width : 300,
        height: 120,
        body  : { autoheight: true, css:"wrap", borderless: true,
            template: mcService.insertIco("fa-info-circle") + mcService.Lang(175)} // "175":"Используйте для входа
    });

    var loginBody = {
        id   : 'loginForm',
        view : 'form',
        rules:{
            "loginField"    : webix.rules.isNotEmpty,
            "passwordField" : function (pass) {
                var login = $scope.loginField.getValue().toString().replace('\\', '/');

                return login.indexOf('/') !== -1 ? !!mcService.trim(pass) : true;
            }
        },
        elements:[ { rows: [
            { cols: [ // login
                { id: 'loginField', name: 'loginField', view: "text", labelWidth : labelWidth, value: $scope.login, required: true,
                    invalidMessage: "<span class='red h10'>" + mcService.Lang(66) + "</span>", // "66" : "Поле не может быть пустым",
                    placeholder: mcService.Lang(1), // "1" : "Логин"
                    on: {
                        onKeyPress : function (code) {
                            if (code === 13) {
                                if ($scope.passwordField.getValue() !== ""){
                                    $scope.loginBtnClick.call($scope.loginForm);
                                } else {
                                    $scope.passwordField.focus();
                                }

                                return false;
                            }
                        }
                    }
                },

                { view: "button", template : mcService.insertIco('fa-question'), width: 34, borderless: true, css: "finger webixBlue loginHelper", click: function(){
                    loginPopup.show(this.getNode());
                }}
            ]},

            { cols: [ // password
                { id: "capsDetect", name: "capsDetect", view: "label", label: "CAPS", width: 42, css: "_info", hidden: true},

                { id: "passwordField", name: 'passwordField', view: "text", labelWidth: labelWidth, type: 'password', /*required: true,*/ keyPressTimeout: 100,
                    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);
                        }
                    }
                },

                { id: 'loginDispLang', template: 'EN', width: 34, css: 'loginHelper', borderless: true}
            ]},

            { height: 38}
        ]}]
    };

    var register = {
        id   : 'registrationForm',
        view : 'form',
        hidden: true,
        rules:{
            "regLoginField"    : webix.rules.registerLogin,
            "regPasswordField" : webix.rules.isNotEmpty,
            "regEmailField"    : webix.rules.isEmail
        },
        elements:[ { rows: [
            { id: 'regLoginField', name: 'regLoginField', view: "text", value: '', required: "true",
                placeholder: mcService.Lang(56), // "56" :"Ваш логин",
                invalidMessage: "<span class='red h10'>" + mcService.Lang(581) + "</span>", // "581":"Поле пустое или содержит недопустимые символы",
                on: {
                    onKeyPress: $scope.checkRegisterChars
                }
            },

            { id: 'regPasswordField', name: 'regPasswordField', view: "text", type: 'password', required: "true",
                placeholder: mcService.Lang(58), // "58" : "Введите ваш пароль",
                invalidMessage: "<span class='red h10'>" + mcService.Lang(66) + "</span>", // "66" : "Поле не может быть пустым",
                on: { onKeyPress : $scope.keyRegister }
            },

            { id: 'regEmailField', name: 'regEmailField', view: "text", type: 'email',
                placeholder: mcService.Lang(59), // "59" : "Введите ваш Email",
                invalidMessage: "<span class='red h10'>" + mcService.Lang(66) + "</span>", // "66" : "Поле не может быть пустым",
                on: { onKeyPress : $scope.keyRegister }
            }
        ]}]
    };

    var serverList = { view: "form",
        rows: []
        .concat(!$scope.isWebClient ? { cols: [
            { id: "loginSelectServer"  + $scope.Name, view: "richselect", options: $scope.serverList, value: $scope.serverList.length ? $scope.serverList[0].id : 0 ,
                placeholder   : mcLang(515), // "515":"Не найден сервер для подключения",
                on: {
                    onFocus    : function () {
                        $scope.loginSelectServer.blur();
                    },
                    onItemClick: $scope.checkEmptyList,
                    onChange   : $scope.changeServerHost
                }
            },

            { id: "toggleServerInfo", view: "button", type: "icon", icon: "gear", width: 34, css: "iconNoPadding", tooltip: mcLang(516), // "516":"Настроить сервер для подключения",
                click: $scope.showServerManager
            }
        ]} : {
            hidden: !$scope.isWebClient, cols: [ // server password
                { id: 'servPassField', view: "text", type: 'password', //labelWidth: labelWidth,
                    placeholder: mcService.Lang(3), // "3" : "Пароль к серверу"
                    on   : {
                        onKeyPress : $scope.keyLogin
                    }
                },

                { view: "button", template: mcService.insertIco('fa-question'), width: 34, borderless: true, css: "finger webixBlue loginHelper", click: function(){
                    srvPwdPopup.show(this.getNode());
                }}
            ]})
        .concat(
            { id: "rememberField", view : "checkbox", labelWidth: 0, value: $scope.remember, css: "finger",
                labelRight: $scope.isWebClient ? mcLang(4) : mcLang(533), // "4":"Запомнить меня на этом компьютере", "533":"Автоматически подключаться",
                on  : { onChange: function(newV){
                    $scope.remember = mcService.convertIntToBool(newV);
                }}
            },

            { view: "button", value: mcService.Lang(13), id: "loginBtn", click: function () { // "5" : "Войти" "13" :"Ок",
                var valid = this.getFormView().validate() || $scope.isWebClient;

                if (valid) {
                    if ($$("_loginType").getValue() == "loginForm"){
                        $scope.loginBtnClick.call($scope.loginForm);
                    } else {
                        $scope.registerBtnClick.call($scope.registrationForm);
                    }
                }
            }}
        )
    };

    if (isMobile){
        loginBody.maxWidth = 550;
        loginBody.minWidth = 300;
        register.maxWidth  = 550;
        register.minWidth  = 300;

        login = webix.ui({
            id       : $scope.Name,
            view     : "scrollview",
            css      : "noBGColor",
            body     : { rows: [
                { view: "tabview", id: "_loginType", borderless: true, cells: [
                    { header: mcLang(5),  body: loginBody}, // "5" : "Войти"
                    { header: mcLang(55), body: register }  // "55" : "Регистрация",
                ]},

                serverList
            ]}
        });
    } else {
        if (!$$("loginLang")){
            webix.ui({
                view    : "richselect",
                id      : "loginLang",
                container: "languageChanger",
                value   : $scope.lang,
                css     : "noBGColorAll",
                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: [
                { view: "tabview", id: "_loginType", borderless: true, cells: [
                    { header: mcLang(5),  body: loginBody}, // "5" : "Войти"
                    { header: mcLang(55), body: register }  // "55" : "Регистрация",
                ]},

                serverList
            ]},
            on : {
                onHide: function () {
                    if (!$scope.doLogin) { // todo: mazafaka fix  окно прячется при нажатии на ESC баг вебикса
                        login.show();
                    }
                }
            }
        });
    }

    //webix.debug = true;

    $scope.loginForm     = $$('loginForm');
    $scope.passwordField = $$('passwordField');
    $scope.loginDispLang = $$('loginDispLang');
    $scope.loginField    = $$("loginField");
    $scope.servPassField = $$("servPassField");
    $scope.loginBtn      = $$("loginBtn");
    $scope.capsDetect    = $$("capsDetect");

    $scope.registrationForm = $$('registrationForm');
    $scope.regLoginField    = $$("regLoginField");
    $scope.regPasswordField = $$("regPasswordField");
    $scope.regEmailField    = $$("regEmailField");

    $scope.loginSelectServer = $$("loginSelectServer" + $scope.Name);

    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;

    var serverManager = $rootScope.isWebClient ? null : $rootScope.UNIT.ctrlServersManager;
    var _view         = null;

    // ======= Error Actions ==========

    function restoreByError() {
        mcService.SaveSessionSettings(mcConst.LoginInfo.login, "", "", false);

        mcConst.LoginInfo.pwd     = "";
        mcConst.LoginInfo.servPwd = "";
        mcConst.LoginInfo.rm      = false;

        $scope.show($scope.lang);
    }

    $rootScope.ErrorActions[14] = function(){ // "14"  : "Неверный пароль пользователя",
        if ($rootScope.isWebClient){
            $rootScope.loginFormNotShowed = false;

            mcService.SaveSessionSettings(mcConst.LoginInfo.login, "", "", false);

            mcConst.LoginInfo.pwd     = "";
            mcConst.LoginInfo.servPwd = "";
            mcConst.LoginInfo.rm      = false;

            if (!_view){
                initLogin($scope);
            }

            //webix.ready(function(){
                setTimeout(function(){
                    $scope.passwordField.focus();
                }, 100);
            //});
        } else {
            restoreByError();
        }
    };

    $rootScope.ErrorActions[80] = function () {
        if ($rootScope.isWebClient){
            $rootScope.ErrorActions[14]();  // остался мусор в локал сторажде от предыдущего логина
        } else {
            restoreByError();
        }
    };

    $rootScope.ErrorActions[81] = function (params) {
        $rootScope.$broadcast('ErrorMsg', [81, mcService.Error(81), params, function(){
            $rootScope.ErrorActions[14]();
        }]);
    };

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

    function selectCurrentServer() {
        var ID = serverManager.getCurrentServer().ID || serverManager.getServerIdByOrderPosition(0);

        if (ID) {
            $scope.loginSelectServer.setValue(ID);
        }
    }

    function addServerToList(info) {
        var idx = mcService.findItemInArrayOfObj($scope.serverList, info.ID, "id");

        info = serverManager.addServerInfo(info);

        if (info){
            $scope.serverList[idx === -1 ? $scope.serverList.length : idx] = {
                id   : info.ID,
                ord  : mcService.convertBool(!!info.ServerID),
                value: "<span class='" + (info.ServerID ? "green" : "linkColor") + "'>" + (info.ServName ? info.ServName + ", " : "") + info.ServHost + ": " + info.Port + "</span>"
            };

            if (idx === -1){
                $scope.loginSelectServer.getList().add($scope.serverList[$scope.serverList.length - 1]);
            } else {
                $scope.loginSelectServer.define('options', $scope.serverList);
                $scope.loginSelectServer.refresh();
            }

            $scope.loginSelectServer.getList().sort("ord", "dsc", "int");
        }
    }

    function hideLoginForm() {
        $scope.doLogin = true;

        _view = $$($scope.Name);

        if (_view && _view.isVisible && _view.isVisible()){
            _view.hide();
        }

        $rootScope.loginFormNotShowed = true;

        mcService.hideFrame("languageChanger");
    }

    function showLoginForm(args) {
        var lang = args ? args[0] : mcConst.Lang;
        var onlyChangeLang = $scope.lang != undefined && $scope.lang != lang;

        var token = $location.param('token');

        webix.storage.local.put('admLang', $scope.lang);

        if (token){
            $rootScope.SendCMDToServer([
                mcConst.lockInterface,
                mcConst._CMD_.cs_login_by_token,
                token
            ]);

            window.history.pushState(null, document.title, window.location.pathname + window.location.hash.replace("token=" + token, ""));
        } else

        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;

            var _login  = "";
            var _pwd    = "";
            var _rLogin = "";
            var _rPwd   = "";
            var _rEmail = "";

            if (_view){
                _login  = $scope.loginField      .getValue() || mcConst.LoginInfo.login;
                _pwd    = $scope.passwordField   .getValue();
                _rLogin = $scope.regLoginField   .getValue();
                _rPwd   = $scope.regPasswordField.getValue();
                _rEmail = $scope.regEmailField   .getValue();
            }

            $scope.show.apply(null, args);

            if ($scope.loginField){
                $scope.loginField      .setValue($scope.login || _login);
                $scope.passwordField   .setValue(_pwd   );
                $scope.regLoginField   .setValue(_rLogin);
                $scope.regPasswordField.setValue(_rPwd  );
                $scope.regEmailField   .setValue(_rEmail);
            }
        }
    }

    function acPreLogin(doLogin) {
        var ID = $scope.loginSelectServer.getValue();

        if (ID){
            var server  = serverManager.getServerInfoByID(ID);
            var needPwd = server ? server.ServPass : "";
            var srv_pwd = needPwd && mcService.isString(server.ServPass) ? server.ServPass : "";

            if (needPwd === true && srv_pwd == ""){
                webix.ui({
                    view    : "window",
                    width   : 240,
                    head    : false,
                    position: "center",
                    modal   : true,
                    body: { view  : "form",
                        rules : { "goServPwd" : webix.rules.isNotEmpty },
                        rows  : [
                            { view: "text", type: "password", placeholder: mcLang(3), name: "goServPwd", require: true}, // "3"  :"Пароль к серверу",
                            { cols: [
                                { view: "button", value: mcLang(13), hotkey: "enter", click: function () {// "13" :"Ок",
                                    if (this.getFormView().validate()){
                                        srv_pwd = this.getFormView().getValues()["goServPwd"];

                                        server.ServPass = srv_pwd;

                                        serverManager.addServerInfo(server);

                                        doLogin(srv_pwd);

                                        this.getTopParentView().close();
                                    }
                                }},

                                { view: "button", value: mcLang(33), click: function () {// "33" :"Отмена",
                                    this.getTopParentView().close();
                                }}
                            ]}
                        ]
                    },
                    on: {
                        onShow: function () {
                            this.getChildViews()[0].focus("goServPwd");
                        }
                    }
                }).show();
            } else {
                doLogin(srv_pwd);
            }
        } else {
            webix.alert(mcLang(530)); // "530":"Выберите сервер для подключение!",
        }
    }

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

    $scope.checkRegisterChars = function (key, e) {
        return $rootScope.restrictedSymbols.indexOf(e.key) === -1;
    };

    $scope.keyLogin = function(code , e){
        if (e.getModifierState && e.getModifierState( 'CapsLock' )){
            !$scope.capsDetect.isVisible() && $scope.capsDetect.show();
        } else {
            $scope.capsDetect.isVisible() && $scope.capsDetect.hide();
        }

        if (code === 13 && !e.ctrlKey && !e.shiftKey && !e.altKey) {
            $rootScope.dropConnectOnLogin = false;
            
            if ($$("_loginType").getValue() == "loginForm"){
                $scope.loginBtnClick.call($scope.loginForm);
            } else {
                $scope.registerBtnClick.call($scope.registrationForm);
            }

            return false;
        }
    };

    $scope.keyRegister = $scope.keyLogin;

    $scope.checkEmptyList = function () {
        if ($scope.loginSelectServer.getList().count() == 0){
            $scope.showServerManager();
        }
    };

    $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 (){
        function _go(srv_pwd) {
            var login = $scope.loginField   .getValue().toString().replace('\\', '/');
            var pass  = $scope.passwordField.getValue();

            if (login.indexOf('/') !== -1 && !pass){
                webix.message({ type:"error", text: mcService.Lang(7) , expire : mcConst.errMessageExpire}); // "7" : "Ошибка входа: неправильно введены данные"
            } else {
                $scope.passwordField.blur();
                
                $rootScope.$broadcast('SendCMDToServer' , [
                    mcConst.lockInterface,
                    mcConst._CMD_.Login,

                    login,
                    pass,
                    srv_pwd,

                    mcConst.LoginInfo.rm
                ]);
            }
        }

        var isValid = this.validate();

        if (isValid) {
            mcConst.LoginInfo.login = $scope.loginField.getValue();
            mcConst.LoginInfo.rm    = $scope.remember;

            if ($scope.isWebClient){
                mcConst.LoginInfo.servPwd = $scope.servPassField.getValue();

                _go(mcConst.LoginInfo.servPwd);
            } else {
                acPreLogin(function (pwd) {
                    _go(pwd);
                })
            }
        } else {
            mcConst.LoginInfo.login = $scope.loginField.getValue();
            mcConst.LoginInfo.rm    = $scope.remember;

            webix.message({ type:"error", text: mcService.Lang(7) , expire : mcConst.errMessageExpire}); // "7" : "Ошибка входа: неправильно введены данные"
        }
    };

    $scope.registerBtnClick = function(){
        function _go(pwd) {
            $scope.passwordField.blur();
            
            $rootScope.$broadcast('SendCMDToServer' , [
                mcConst.lockInterface,
                mcConst._CMD_.Register,

                $scope.regLoginField   .getValue(), //mcConst.LoginInfo.login  ,
                $scope.regPasswordField.getValue(), //mcConst.LoginInfo.pwd    ,
                //$scope.servPassField   .getValue(), //mcConst.LoginInfo.servPwd,
                pwd,

                mcConst.LoginInfo.rm,                //mcConst.LoginInfo.rm
                $scope.regEmailField   .getValue(),
                0
            ]);
        }

        var isValid = this.validate();

        if (isValid) {
            mcConst.LoginInfo.login = $scope.regLoginField.getValue();
            mcConst.LoginInfo.rm    = $scope.remember;

            if ($scope.isWebClient){
                _go($scope.servPassField.getValue());
            } else {
                acPreLogin(function (pwd) {
                    _go(pwd);
                })
            }
        }/*else {
            webix.message({ type:"error", text: mcService.Lang(580) , expire : mcConst.errMessageExpire}); // "580":"Логин не может быть пустым и не должен содержать символы: \"\/\\[]:;|=,+*?<>",
        }*/
    };

    $scope.changeLang = function(lang){
        $rootScope.$broadcast('ChangLanguage', mcService.detectLang(lang));
    };

    $scope.changeServerHost = function (val) {
        serverManager.setCurrentServerByID(val);
    };

    $scope.showServerManager = function () {
        hideLoginForm();

        $rootScope.$broadcast('show' + mcConst.dataModels.ServerManager, [function (_srv) {
            showLoginForm();

            mcService.showFrame("languageChanger");

            $scope.loginSelectServer.setValue(_srv.ID || null);

            $scope.getMcList();
            //selectCurrentServer();
        }]);
    };

    $scope.show = function(lang, reconn){
        var invite         = mcService.getSearchQuery(window.location.search, "invite", true);
        var onlyChangeLang = $scope.lang != undefined && $scope.lang != lang;

        $scope.lang  = lang || mcConst.Lang;
        $scope.login = mcConst.LoginInfo.login || "";

        $scope.remember = mcConst.LoginInfo.rm; 

        webix.storage.local.put(mcConst.storageFields.lang, $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.uinwith && (user.uid && user.uid < 0)){
                            $rootScope.SetPrivate(user.uinwith);
                        } else

                        if (user.uid){
                            $rootScope.SetConf(user.uid);
                        }

                        $rootScope.$broadcast('StopPingTimer', []);

                        $rootScope.SendCMDToServer([
                            mcConst.lockInterface,
                            mcConst._CMD_.Login,
                            user.uin,
                            user.pass,
                            "",
                            true
                        ]);
                    } else {
                        $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

                        webix.alert(mcLang(503)); //"503":"Ваше приглашение недействительно",

                        // _view = initLogin($scope);

                        // $scope.getMcList(); // reloadServersList

                        // $rootScope.loginFormNotShowed = false;
                    }
                }
            ]);

        } else

        if (!onlyChangeLang && $scope.remember) {
            $rootScope.loginFormNotShowed = !_view || (_view && _view.isVisible && !_view.isVisible());

            if (reconn){
                // $rootScope.$broadcast('ErrorMsg', [-1, mcLang(18) + "<br><br>" + mcConst.loadGif, '', function(){ // "18" :"Обработка запроса",
                $rootScope.$broadcast('ErrorMsg', ['223', mcService.Error('223') + "<br>" + mcLang(539) + "<br><br>" + mcConst.loadGif, '', function(){ // "539":"Восстанавливаем связь!",
                    $rootScope.$broadcast('show' + mcConst.dataModels.Login, []);
                }]);
            }

            setTimeout(function () {
                $rootScope.$broadcast('SendCMDToServer' , [
                    mcConst.lockInterface,
                    mcConst._CMD_.Login,
                    mcConst.LoginInfo.login  ,
                    mcConst.LoginInfo.pwd    ,
                    mcConst.LoginInfo.servPwd,
                    mcConst.LoginInfo.rm
                ]);
            }, reconn ? 500 : 10);
        } else {
            //webix.ready(function () {
                _view = initLogin($scope);

                $scope.getMcList(); // reloadServersList

                $rootScope.loginFormNotShowed = false;
            //});
        }
    };

    $scope.getMcList = function () { // reloadServersList
        if (!$scope.isWebClient){
            $scope.serverList = [];

            var saveCurrentServer = serverManager.getCurrentServer().ID;

            $scope.loginSelectServer.getList().clearAll();
            $scope.loginSelectServer.setValue();

            serverManager.mapServers(addServerToList);

            if (saveCurrentServer){
                $scope.loginSelectServer.setValue(saveCurrentServer);
            } else {
                var val= serverManager.getServerIdByServID(mcConst.ClientSettings.ServID);

                if (val){
                    $scope.loginSelectServer.setValue(val);
                } else {
                    $rootScope.$broadcast('sendCMDToElectron', [
                        mcConst._CMD_.ce_get_server_list
                    ]);
                }
            }
        }
    };

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

    $scope.$on('addServerHostToList', function (e, args) {
        if (_view && $scope.loginSelectServer){
            for (var srv in args[0]){
                addServerToList.call(null, args[0][srv]);
            }

            selectCurrentServer();
        }
    });

    $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 initCommonContacts($scope){
    "use strict";

    return __treeContacts($scope, mcLang(10)); // "10" :"Общие контакты",
}
function commonContactsController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.CommonContacts;

    var _view = null;
    var state = null;
    var userStatuses = null;

    $scope.filterField = null;
    $scope.data = [];

    function fillContacts(data){
        data.forEach(function(contact){
            if (contact.UIN !== -1) {
                contact.DisplayName = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [contact.UIN]).DisplayName;
            }
        });

        _view.clearAll();
        _view.filter();
        _view.parse(mcService.convertDataToTree(data));

        $scope.data = data;

        if (userStatuses){
            mcService.updateStatuses(userStatuses, _view.data);
        }

        filterOnline();

        $rootScope.Storage.dialogs.load(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.COMMONTREE, function (info) {
            info = mcService.StringToObj(info);
            
            if (mcService.isObject(info) /*&& info.CommonContactsState*/) {
                state = info; //.CommonContactsState;

                state && _view.setState(state);

                if (!state) {
                    state = _view.getState();
                }
            }
        });
    }

    function filterOnline(){
        if (_view && mcConst.ClientSettings.GeneralOnlyOnlineInCommonContacts) {
            _view.filter(function (obj) {
                return obj.onlineState >= mcConst.states.online;
            });
        }
    }

    function refreshList(data){
        userStatuses = data;

        if (_view && _view.data){
            mcConst.ClientSettings.GeneralOnlyOnlineInCommonContacts && _view.filter();

            mcService.updateStatuses(userStatuses, _view.data);

            filterOnline();
        }
    }

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

    $scope.changeState = function(){
        $rootScope.Storage.contactsTree.save(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.COMMONTREE, JSON.stringify(_view.getState()));
    };

    $scope.openPrivate = function(id){
        var uin = _view.getItem(id).UIN;

        if (uin){
            $rootScope.$broadcast('OpenPrivate', [uin]);
        }
    };

    $scope.pinTool = function(){
        $rootScope.$broadcast($scope.pinBtn.getValue() ? 'pinTool' : 'unPinTool', [$scope.Name]);
    };

    $scope.getData = function(){
        $rootScope.$broadcast('SendCMDToServer' , [
            mcConst._CMD_.cs_get_common_contacts_list,
            mcConst.SessionID,
            fillContacts
        ]);
    };

    $scope.show = function(){
        $rootScope.setTool($scope.Name);

        _view = initCommonContacts($scope);

        if (!$scope.filterField){
            $scope.filterField = $$("treeContacts" + $scope.Name);
            $scope.filterField.onClear(filterOnline);
        }
    };

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

    var _msg = window._messages_.CommonContacts = {
        allUsersOnlineStatusesChanged   : 'allUsersOnlineStatusesChanged',
        newCommonContactList            : 'newCommonContactList',
        updateCommonContactInfo         : 'updateCommonContactInfo',
        clickPinBtn                     : 'clickPinBtn',
        // changeSort                      : 'changeSort'
    };

    /*$scope.$on(_msg.changeSort, function(){
        if (_view){
            _view.sort(function (a, b) {
                if (!a.folder && !b.folder){
                    if (mcConst.ClientSettings.GeneralContactsSortStyle == 1){
                        return mcService.sortAB(a.DisplayName, b.DisplayName);
                    } else {
                        return mcService.sortAB(a.UIN, b.UIN);
                    }
                } else {
                    return 0;
                }
            });

            _view.refresh();
        }
    });*/

    $scope.$on(_msg.clickPinBtn, function(e, args){
        if (args[0] === $scope.Name){
            $scope.pinBtn.setValue(1);
        }
    });

    $scope.$on(_msg.updateCommonContactInfo, function(e, args){
        var uin  = args[0];
        var info = args[1];

        if (_view){
            var user = _view.getItem(uin);

            if (user){
                _view.parse(mcService.Marge(user, info));
            }
        }
    });

    $scope.$on(_msg.allUsersOnlineStatusesChanged, function(e, args){
        refreshList.apply(null, args);
    });

    $scope.$on(_msg.newCommonContactList, function(e, args){
        if (_view){
            state = _view.getState();

            fillContacts.apply(null, args);
        }
    });

    $scope.$on('hide' + $scope.Name, function(){
        if (_view){
            state = _view.getState();

            _view.getParentView().hide();
        } else {
            state = null;
        }
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        $scope.show();
        $scope.getData();
    });
}

function initPersonalContacts($scope){
    "use strict";

    var view = __treeContacts($scope, mcLang(17)); // "17" :"Личные контакты",

    var addNewGroup = $$("addNewGroup" + $scope.Name);

    if (!addNewGroup && $scope._rights.QContactsListChange){
        view.parentContainer.addView({
            view : "button",
            id   : "addNewGroup" + $scope.Name,
            value: mcLang(561), // "561":"Добавить группу",
            click: function () {
                $scope.contextPersonalContacts.emitAction('createGroup');
            }
        });
    }

    if (!$scope._rights.QContactsListChange && addNewGroup){
        view.parentContainer.removeView("addNewGroup" + $scope.Name);
    }

    if (!$scope.contextPersonalContacts && $scope._contextMenuList && $scope._rights.QContactsListChange){
        $scope.contextPersonalContacts = $scope._contextMenuList.editButtons({
            buttons : []
                .concat([{
                    name           : 'createGroup',
                    type           : 'add', // add, edit, del
                    confirm        : $scope.contextMenu.addGroup,
                    addToContext   : true,
                    showedInList   : false,
                    contextCss     : 'byCenter whiteAll noUpperCase',
                    label          : mcLang(561), // "561":"Добавить группу",
                    title          : mcLang(561), // "561":"Добавить группу",
                    editTitle      : mcLang(35),  // "35" :"Название",
                    okTitle        : mcLang(77),  // "77" :"Сохранить",
                    cancelTitle    : mcLang(33)   // "33" :"Отмена",
                }])
                .concat([{
                    name           : 'addContact',
                    type           : 'add', // add, edit, del
                    confirm        : $scope.contextMenu.addUser,
                    addToContext   : true,
                    showedInList   : false,
                    contextCss     : 'byCenter whiteAll noUpperCase',
                    label          : mcLang(562), // "562":"Добавить контакт",
                    title          : mcLang(562), // "562":"Добавить контакт",
                    editTitle      : mcLang(564), // "564":"Укажите UIN",
                    okTitle        : mcLang(77),  // "77" :"Сохранить",
                    cancelTitle    : mcLang(33)   // "33" :"Отмена",
                }])
                .concat([{
                    name           : 'editGroup',
                    type           : 'edit', // add, edit, del
                    confirm        : $scope.contextMenu.renameGroup,
                    addToContext   : true,
                    showedInList   : false,
                    contextCss     : 'byCenter whiteAll noUpperCase',
                    label          : mcLang(563), // "563":"Переименовать",
                    title          : mcLang(563), // "563":"Переименовать",
                    editTitle      : mcLang(35),  // "35" :"Название",
                    okTitle        : mcLang(77),  // "77" :"Сохранить",
                    cancelTitle    : mcLang(33)   // "33" :"Отмена",
                }])
                .concat([{
                    name           : 'delItem',
                    type           : 'del', // add, edit, del
                    confirm        : $scope.contextMenu.del,
                    addToContext   : true,
                    showedInList   : false,
                    contextCss     : 'byCenter whiteAll noUpperCase',
                    delName        : "DisplayName",
                    label          : mcLang(519), // "519":"Удалить",
                    cancelTitle    : mcLang(33),   // "33" :"Отмена",
                    title          : mcLang(565)   // "565":"Удалить <br>%s?",
                }]),

            fillItems     : ["DisplayName"],
            list          : $scope.Name,
            onShowContext : $scope.onShowPopMenu,
            onHideContext : $scope.removeHotKeys,
            contextOpt    : {
                width: 170,
                padding: 0,
                btnHeight: 30
            }
        });

        $scope.contextPersonalContacts.attachContext();
    }

    if ($scope.contextPersonalContacts && $scope._contextMenuList && !$scope._rights.QContactsListChange) {
        $scope.contextPersonalContacts.destroy();
        
        $scope.contextPersonalContacts = null;
    }

    return view;
}
function personalContactsController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.PersonalContacts;

    var _view = null;
    var state = null;
    var userStatuses = null;

    $scope._rights = {
        QContactsListChange: false
    };
    $scope._contextMenuList = window.mcWidgets.ButtonsListWithContext ? new window.mcWidgets.ButtonsListWithContext($rootScope) : null;
    // =======================================================

    function fillContactList(data){
        if (_view){
            data.forEach(function(contact){
                if (contact.UIN !== -1) {
                    contact.DisplayName = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [contact.UIN]).DisplayName;
                }
            });
            
            _view.clearAll();
            _view.parse(mcService.convertDataToTree(data));

            $scope.data = _view.data;

            if (userStatuses){
                mcService.updateStatuses(userStatuses, $scope.data);
            }

            if (state) {
                _view.setState(state);
            } else {
                state = _view.getState();
            }

            $rootScope.Storage.options.load(function (info) {
                if (mcService.isObject(info) && info.PersonalContactsState) {
                    state = info.PersonalContactsState;
                    
                    _view.setState(state);
                }
            });
        }
    }

    function show(container){
        $scope.container = container;

        $scope.show();
        $scope.getData();
    }

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

    $scope.registerHotKeys = function () {
        $rootScope.hotKeyDispatcher.addPreset($scope.Name, [{
            lockPrev: true,
            key   : mcConst.keyCodes.esc,
            func  : function hidePopupPersonalContacts() {
                $scope.contextPersonalContacts.hideContext();
            }
        }], document, true);
    };

    $scope.removeHotKeys = function () {
        setTimeout(function () {
            $rootScope.hotKeyDispatcher.removePreset($scope.Name);
        }, 10);
    };

    $scope.changeState = function(){
        $rootScope.Storage.contactsTree.save(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.COMMONTREE, JSON.stringify(_view.getState()));
        // $rootScope.Storage.options.save({
        //     PersonalContactsState : _view.getState()
        // });
    };

    // $scope._template = function (obj, common) {
    //     var res  = "";
    //     var name = "";
    //
    //     if (obj.folder) {
    //         var color = obj.$level > 1 ? "#00720d" : "black";
    //
    //         name = (obj.$level > 1 ? mcService.insertIco("fa-users", "smaller") : "") + "<span class='treeBranchContacts webix_tree_item' style='color:" + color + ";'>" + obj.DisplayName + "</span>";
    //     } else
    //
    //     if (obj.TeamLead) {
    //         name = mcService.insertIco("fa-user", "smaller treeAvatar") + "<span class='teamlead toLeft' style='color: rgb(12, 132, 228)'>" + obj.DisplayName + "</span>";
    //     } else {
    //         name = mcService.insertIco("fa-user", "smaller treeAvatar") + "<span class='toLeft'>" + obj.DisplayName + "</span>";
    //     }
    //
    //     res = mcService.tree_icon(obj) + "<span class='webix_tree_item ' style='cursor: pointer;'>" + name;
    //
    //     if (!obj.folder) {
    //         res += " <span style='" + (mcService.isFuckingFirefox() ? "" : "float:right; ") + "color:#666; magrin-rigth: 19px; padding: 0; background: none; font-size: 85%'> uin: " + obj.UIN + "</span>";
    //     }
    //
    //     res += "</span>";
    //
    //     // return common.space(obj) + res;
    //     return mcService.myReplaceFormated(
    //         common.space(obj) + mcService.tree_icon(obj) + "<span class='webix_tree_item ' style='cursor: pointer;' title='#{title}'>#{branch}#{user}</span>",
    //         {
    //             branch : obj.folder ? "<span class='treeBranchContacts webix_tree_item' style='color: #00720d;'>" + obj.DisplayName + "</span>" : "",
    //             user   : obj.folder ? "" : mcService.insertIco("fa-user", "smaller") + obj.DisplayName,
    //             title  : obj.folder ? "" : obj.DisplayName + ", UIN: " + obj.UIN
    //         }
    //     );
    // };

    $scope.onShowPopMenu = function () {
        var context = this.getContext();

        context.obj.select(context.id);

        var uu = context.obj.getItemNode(context.id);

        $scope.registerHotKeys();

        this.getChildViews()[0].getChildViews()[2][context.obj.getItem(context.id).IsGroup ? "enable" : "disable"]();

        this.blockEvent();

        this.show(uu, {x: 0, y: 12}, "bottom");

        this.unblockEvent();
    };

    $scope.contextMenu = {
        addGroup: function (edit) {
            var form  = this;
            var value = mcService.trim(edit.getValue());

            if (form.validate() && value){
                state = _view.getState();

                $rootScope.SendCMDToServer([
                    mcConst._CMD_.cs_add_personal_contact_group,
                    mcConst.SessionID,
                    
                    value
                ]);

                $scope.getData();
            } else {
                return true;
            }
        },
        addUser : function (edit, btns, item) {
            var form = this;
            var uin  = parseInt(edit.getValue());

            if (form.validate() && !isNaN(uin)){
                state = _view.getState();

                $rootScope.SendCMDToServer([
                    mcConst._CMD_.cs_add_new_contact,
                    mcConst.SessionID,

                    uin,
                    item.IsGroup ? item.GroupID : item.ParentID
                ]);

                $scope.getData();
            } else {
                webix.message(mcLang(566)); // "566":"Укажите числовой UIN пользователя",

                setTimeout(function(){
                    edit.focus();
                }, 20);

                return true;
            }
        },
        del     : function (item) {
            state = _view.getState();

            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_delete_id_from_personal_contacts,
                mcConst.SessionID,

                item.id
            ]);

            $scope.getData();
        },
        renameGroup: function (edit, btns, item) {
            var form  = this;
            var value = mcService.trim(edit.getValue());

            if (form.validate() && value){
                state = _view.getState();

                $rootScope.SendCMDToServer([
                    mcConst._CMD_.cs_rename_contacts_group,
                    mcConst.SessionID,

                    item.GroupID,
                    value
                ]);

                $scope.getData();
            } else {
                return true;
            }
        }
    };

    $scope.openPrivate = function(id){
        var uin = _view.getItem(id).UIN;

        if (uin){
            $rootScope.$broadcast('OpenPrivate', [uin]);
        }
    };

    $scope.pinTool = function(){
        $rootScope.$broadcast( $scope.pinBtn.getValue() ? 'pinTool' : 'unPinTool', [$scope.Name]);
    };

    $scope.getData = function(){
        $rootScope.$broadcast('SendCMDToServer' , [
            mcConst._CMD_.cs_get_contacts_list,
            mcConst.SessionID,

            fillContactList
        ]);
    };

    $scope.show = function(){
        $rootScope.setTool($scope.Name);

        _view = initPersonalContacts($scope);
    };

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

    var _msg = window._messages_.PersonalContacts = {
        allUsersOnlineStatusesChanged   : 'allUsersOnlineStatusesChanged',
        clickPinBtn                     : 'clickPinBtn'
    };

    $scope.$on(_msg.clickPinBtn, function(e, args){
        if (args[0] === $scope.Name){
            $scope.pinBtn.setValue(1);
        }
    });

    $rootScope.$on(_msg.allUsersOnlineStatusesChanged, function(e, args){
        userStatuses = args[0];

        mcService.updateStatuses(userStatuses, $scope.data);

        if (_view){
            _view.refresh();
        }
    });

    $scope.$on('hide' + $scope.Name, function(){
        if (_view){
            state = _view.getState();

            _view.getParentView().hide();
        } else {
            state = null;
        }
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        show.apply(null, args);
    });

    $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
        $scope._rights.QContactsListChange = mcService.checkRights(mcConst._CMD_.RS.QContactsListChange);

        if (_view && _view.isVisible()){
            show($scope.container);
        }
    });
}

function initConfUserList($scope){
    "use strict";

    var __view = $$($scope.Name);

    if (!__view){
        var contacts = { id: $scope.Name, rows: [
            { height: 36, cols: [
                { id: "confLabelCount", template: mcLang(703), css: 'byCenter noBG lHeight38', borderless: true}, // "703":"Участники",
                { id: "leaveConfBtn", view:"button", type:"icon", icon: "sign-out", width: 30, css: "btnPaddingToggle4", paddingX: 1, hidden: !$scope._rights.QAllowExitTextConfs,
                    click: $scope.leaveConf,
                    tooltip: mcLang(28)} // "28" :"Выйти из конференции",
            ]},

            new mcService.FilterFiled({
                listName     : "userList" + $scope.Name,
                css          : "noBorderAll",
                noClearBtn   : true,
                placeholder  : mcLang(48), // "48" :"Поиск...",
                filterParams : ['DisplayName']
            }),

            {
                view      : "datatable",
                id        : "userList" + $scope.Name,
                navigation: false,
                data      : $scope.data,
                css       : "noBGColor noPaddingList noBorderBottomRow smallListHeight",
                select    : "row",
                resizeColumn: false,
                scroll    : isMobile,
                scheme    : {
                    $sort  : $scope.sortList
                },
                borderless: true,
                rowHeight : 22,
                onContext : {},
                header    : false,
                columns   : [
                    { id:"DisplayName", fillspace: true, template  : $scope.cnView}, // "1895":"Отображаемое имя",
                ],
                on : {
                    onItemClick: $scope.openPrivate
                }
            }
        ]};

        $$($scope.container).addView(contacts);

        __view = $$($scope.Name);

        $scope.confUserList   = $$("userList" + $scope.Name);
        $scope.confLabelCount = $$("confLabelCount");
        $scope.leaveConfBtn   = $$("leaveConfBtn");

        mcService.disableContextMenu(__view);

        $scope.contextConfUser = webix.ui({
            view   : "context",
            width  : 250,
            master : $scope.confUserList,
            id     : "contextConfUser" + $scope.Name,
            css    : "noUpperCase",
            padding: 0,
            height : 90,
            body   : { padding: 0, rows: [
                { id: "getUserInfo" + $scope.Name, view: "button", type: "icon", icon: "info-circle", css: "icoButton", height: 27,
                    click: $scope.getUserInfoFromContext, label: mcService.Lang(721)}, // "721":"Профиль пользователя",

                { id: "openPrivate" + $scope.Name, view: "button", type: "icon", icon: "comments-o", css: "icoButton", height: 27,
                    click: $scope.openDialogFromContext, label: mcService.Lang(685)}, // "685":"Открыть диалог",

                { id: "inviteButtonMenu" + $scope.Name, view: "button", type: "icon", icon: "plus", css: "icoButton", height: 27, label: mcLang(714), // "714":"Пригласить в конференцию",
                    hidden: $scope._rights.QAllowInvites,
                    click: function(id, e){
                        var context = this.getTopParentView();
                        var uin     = context.getContext().id;

                        if (uin){
                            e.preventDefault();

                            $scope.fillConfList(uin);
                            $scope.confToInvite.show(this.getNode(), {pos: "right", y: -5});
                        }
                    }
                },
            ]},
            on: {
                onShow: function(){
                    var context = this.getContext();
                    var uu      = context.obj.getItemNode(context.id);

                    $scope.contextUser = context.id;

                    if ($scope.contextUser == 0 || $scope.contextUser == mcConst.UserInfo.UIN){
                        this.hide();
                    } else {
                        this.blockEvent();

                        this.show(uu, {y: 12}, "bottom");

                        this.unblockEvent();
                    }
                },
                onBeforeShow: function (e) {
                    for (var i = 0; i < e.path.length - 1; i++){
                        if (e.path[i].nodeName === "BODY") break;

                        var uin = e.path[i].getAttribute("webix_l_id");
                        
                        if (uin) {
                            $scope.confToInvite.config.uin = uin;

                            break;
                        }
                    }

                    if (uin) {
                        $scope.inviteButtonMenu[!$scope._rights.QAllowInvites || this.webSup || this.uin === "0" ? "hide" : "show"]();
                    }
                }
            }
        });

        $scope.confToInvite = webix.ui({
            view     :"submenu",
            id       :"confToInviteSubMenu" + $scope.Name,
            padding  : 0,
            autowidth: true,
            template : function(obj){
                return "<div style='margin-left: -5px; margin-right: 0; position: relative'>" +
                    (obj.userCount ? "<span class='userCount' style='color: gray'>(" + obj.userCount + ")</span> " : "") + (obj.Name || obj.ConfName) +
                    "</div>";
            },
            type     : { height: 26 },
            on       : { onItemClick: $scope.inviteToConf }
        });

        $scope.inviteButtonMenu = $$("inviteButtonMenu" + $scope.Name);
    } else {
        __view.show();

        $scope.confUserList.clearAll();
        $scope.confUserList.parse($scope.data);
    }

    return __view;
}
function confUserListController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.ConfUserList;

    var _view = null;
    var UID   = 0;

    $scope.data = [];
    $scope.contextUser = null;
    $scope._rights = {
        QAllowViewRemoteUserProfile: false,
        QAllowInvites: false,
        QViewTextConfMembersList: false
    }

    function removeUsers(uins) {
        uins = mcService.isArray(uins) ? uins : uins.toString().split(',');

        uins.forEach(function (uin) {
            if ($scope.confUserList && $scope.confUserList.getItem(uin)) {
                $scope.confUserList.remove(uin);
            }

            mcService.delArrayItem($scope.data, mcService.findItemInArrayOfObj($scope.data, uin, "UIN"));
        });

        setUserCount();
    }

    function clearUser() {
        if ($scope.confUserList){
            $scope.confUserList.clearAll();

            setUserCount();
        }
    }

    function setUserCount (){
        if ($scope.confLabelCount) {
            var online = 0;

            $scope.data.forEach(function(user) {
                if ($scope.allStatuses[user.UIN] >= 0){
                    online ++;
                }
            });

            var offlineCount = $rootScope.$broadcast(window._messages_.dialogsList.getUserConfCount, [UID]) - online;

            if (offlineCount < 0) offlineCount = 0;

            $scope.confLabelCount.define("template",
                "<span style='position: relative'>" +
                mcLang(703) +                           // "703":"Участники",
                " (<span class='stateCircleInline' style='background-color: " + mcService.getColorForStatus(mcConst.states.online) + "'></span>" +
                online + "/<span class='stateCircleInline' style='background-color: " + mcService.getColorForStatus(mcConst.states.offline) + "'></span>" + offlineCount + ")</span>"
            );
            $scope.confLabelCount.refresh();

            //$rootScope.$broadcast(window._messages_.chatFrame.updateFrameConfCount, [UID, $scope.confUserList.count()]);
        }
    }

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

    $scope.fillConfList = function(_uin) {
        var list = $rootScope.$broadcast(window._messages_.dialogsCtrl.getChannelsList);
        var uin  = _uin.split('-')[1];

        $scope.confToInvite.define("data", list);
        $scope.confToInvite.refresh();
        $scope.confToInvite.sort("id", "asc");
        $scope.confToInvite.config.UIN = uin;
    };

    $scope.openDialogFromContext = function(){
        if ($scope.contextUser) {
            $rootScope.$broadcast('OpenPrivate', [$scope.contextUser]);
        }

        $scope.contextConfUser.hide();
    };

    $scope.getUserInfoFromContext = function (){
        if ($scope.contextUser) {
            if ($scope._rights.QAllowViewRemoteUserProfile){
                $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.UserProfile, $scope.contextUser]);
                $rootScope.$broadcast("selectTool");
            }
        }

        $scope.contextConfUser.hide();
    };

    $scope.inviteToConf = function(item) {
        var uin = $scope.confToInvite.config.uin;
        var uid = item.split("-")[1];

        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_forcibly_join_to_text_channel,
            mcConst.SessionID,

            uin,
            uid
        ]);

        $scope.contextConfUser.hide();
    };

    $scope.cnView = function(obj){
        // var isWebSup = obj.DisplayName.indexOf("(websup)") === 0;
        var rColor   = $rootScope.colorPalette[obj.UIN % $rootScope.colorPalette.length];
        var name     = obj.DisplayName.replace("(websup)", "<span class='bolder brown'>W</span>&nbsp;");
        var _status   = $rootScope.$broadcast(window._messages_.dialogsList.getUserState, [obj.UIN]);

        return mcService.myReplaceFormated(
            // '<span class="circleAvatarSmall shadow2px boxShadow" style="background-color: ##{rColor}; opacity: .7;">#{icon}</span> ' +
            '<span class="shadow1px boxShadow #{status}" style="#{statusColor};">&nbsp;</span> ' +
            '<span class="#{moder}">#{name}</span>',
            {
                // icon       : isWebSup ? mcService.getTwoLettersName(total.nick.replace("(websup)", "")) : mcService.getTwoLettersName(name),
                rColor     : rColor,
                moder      : obj.Moder ? "red" : "",
                status     : mcConst.storageOpts.STATUSICO + obj.UIN,
                statusColor: "background-color:" + mcService.getColorForStatus(_status),
                name       : name
            }
        )
    };

    $scope.openPrivate = function(id){
        var uin = $$(this).getItem(id).UIN;

        if (uin != mcConst.UserInfo.UIN){
            $rootScope.$broadcast('OpenPrivate', [uin]);
        }

        return false;
    };

    $scope.leaveConf = function () {
        $rootScope.$broadcast('leaveConf', [$rootScope.GetChatID()]);
    };

    $scope.getData = function(){
        UID = $rootScope.GetChatID();

        $rootScope.$broadcast('getConfUsersList', [
            UID,

            function(data){
                $scope.data = data;

                // console.log(data);

                $scope.show();
            }
        ]);
    };

    $scope.sortList = function (a, b){ // єбіческа сортировка
        var onlineA = $scope.allStatuses[a.UIN] >= 0;
        var onlineB = $scope.allStatuses[b.UIN] >= 0;
        var offlineA= $scope.allStatuses[a.UIN] === -1 || $scope.allStatuses[a.UIN] === undefined;
        var offlineB= $scope.allStatuses[b.UIN] === -1 || $scope.allStatuses[b.UIN] === undefined;

        return offlineA && offlineB
            ? mcService.sortAB(a.DisplayName, b.DisplayName)
            : onlineA && onlineB
                ? mcService.sortAB(a.DisplayName, b.DisplayName)
                : onlineA && offlineB
                    ? -1
                    : offlineA && onlineB
                        ? 1
                        : 0
        ;
    };
    
    $scope.show = function(){
        if ($rootScope.GetChatType() === 'UID'){
            if (!$scope.allStatuses) {
                $scope.allStatuses = $rootScope.$broadcast(window._messages_.dialogsList.getLastUserStates);
            }

            $rootScope.setTool($scope.Name);

            _view = initConfUserList($scope);

            setUserCount();

            $scope.checkOnline();
        }
    };

    $scope.checkOnline = function () {
        if (mcConst.ClientSettings.GeneralOnlyOnlineInCommonContacts) {
            $scope.confUserList.filter(function (obj) {
                return  $rootScope.$broadcast(window._messages_.dialogsList.getUserState, [obj.UIN]) !== mcConst.states.offline;
            });

            $scope.confUserList.refresh();
        }
    };

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

    var _msg = window._messages_.confUsers = {
        allUsersOnlineStatusesChanged: 'allUsersOnlineStatusesChanged',
        removeUserFromConf  : 'removeUserFromConf',
        addUserToConf       : 'addUserToConf',
        clearUserForUID     : 'clearUserForUID',
        updateConfUserInfo  : 'updateConfUserInfo'
    };

    $scope.$on(_msg.updateConfUserInfo, function (e, args) {
        var uin  = args[0];
        var info = args[1];
        var user = $scope.confUserList.getItem(uin);

        if (user){
            user = mcService.Marge(user, info);
            
            $scope.confUserList.updateItem(uin, user);
        }
    });

    $scope.$on(_msg.clearUserForUID, function () {
        clearUser();
    });

    $scope.$on(_msg.allUsersOnlineStatusesChanged, function(e, args){
        var newState  = args[0];

        if ($scope.confUserList) {
            $scope.allStatuses = $rootScope.$broadcast(window._messages_.dialogsList.getLastUserStates);

            mcService.updateStatuses(newState, $scope.data);

            $scope.confUserList.sort($scope.sortList);

            setUserCount();

            $scope.checkOnline();
        }
    });

    $scope.$on(_msg.addUserToConf, function(e, args){
        if (_view){
            var users = args[0];

            // console.log("addUserToConf " + JSON.stringify(users))

            $scope.confUserList.parse(users);

            if (!$scope.data.length){
                $scope.data = users;
            }

            setUserCount();

            $scope.checkOnline();
        } else {
            console.log("addUserToConf no view")
        }
    });

    $scope.$on(_msg.removeUserFromConf, function(e, args){
        removeUsers.apply(null, args);
    });

    $scope.$on('hide' + $scope.Name, function(){
        if (_view){
            _view.hide();
        }
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        if ($scope._rights.QViewTextConfMembersList) {
            $scope.getData();
        }
    });

    $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
        $scope._rights.QAllowViewRemoteUserProfile  = mcService.checkRights(mcConst._CMD_.RS.QAllowViewRemoteUserProfile);
        $scope._rights.QAllowInvites                = mcService.checkRights(mcConst._CMD_.RS.QAllowInvites);
        $scope._rights.QViewTextConfMembersList     = mcService.checkRights(mcConst._CMD_.RS.QViewTextConfMembersList);
        $scope._rights.QAllowExitTextConfs          = mcService.checkRights(mcConst._CMD_.RS.QAllowExitTextConfs);

        if (_view && !$scope._rights.QViewTextConfMembersList){
            _view.hide();
        }

        $scope.inviteButtonMenu && $scope.inviteButtonMenu [$scope._rights.QAllowInvites       ? "show" : "hide"]();
        $scope.leaveConfBtn     && $scope.leaveConfBtn     [$scope._rights.QAllowExitTextConfs ? "show" : "hide"]();
    });

}

function initPrivateInfo($scope){
    "use strict";

    var __view = $$($scope.Name);
    var uAct = "userActions";

    if (!__view){
        $$($scope.container).addView({ id: $scope.Name, rows: [
            { id: uAct + "SwitchTool", view: "segmented", css:"yellowSelect noBorderAll noCurve", value: 2, on: { onChange: $scope.changeView }, options:
                []
                .concat(mcConst.UserInfo.hasInvite ? [] : [{ id: "1", value: mcLang(538)}]) // "538":"Действия",
                .concat([{ id: "2", value: mcLang(537)}]), // "537":"Информация",
            },

            { id: 'uFoto', template: ' ', height: 175, css: "byCenter noBGColor framePaddingW10 fotoLineHeight", borderless: true},
            { height: 5 },

            { id: "userInfo" + $scope.Name, rows: [
                {id: 'uPrivateInfo', template: 'User Info', scroll: "y", css: "scrollMenu noBGColor selectable", borderless: true},

                { height: 40, cols: [
                    { template: "<div id='uPrivateTypingNotify'><img id='animateTypingNotify' class='hidden' src='" + mcConst.imagesPath.all + "pero.gif' ></div>", width: 32, height: 30, padding: 0, borderless: true, css: "noBGColor"},
                    { rows: [
                        { id: 'uPrivateState', template: mcService.Lang(29), css: "noBGColor framePaddingW10", height: 18, borderless: true}, // "29" : "Не в сети",
                        { id: 'uPrivateTypingTextNotify', template: ' ', borderless: true, css: "noBGColor framePaddingW10", height: 24}
                    ]}
                ]}
            ]},

            { id: uAct + $scope.Name, hidden: true, rows:[
                { id: uAct + "DisplayName", view: "template", template: " ", borderless: true, autoheight: true, css: "noBGColor"},
                { height: 2 },

                { view: "scrollview", borderless: true, css: "noBGColor scrollMenu", body: { rows: [
                    { view: "button", id: uAct + "SendDirect",  type: "icon", icon: "exchange", label: mcLang(582), click: $scope.directSend,
                        hidden: $scope.isWeb || mcConst.UserInfo.hasInvite, css: "btnNoPadding noUpperCase 1byCenter", height: 30}, // "582":"Передать файлы",

                    { view: "button", id: uAct + "RedirectBtn", type: "icon", icon: "reply",    label: mcLang(618), click: $scope.showRedirect,
                        hidden: !$scope._rights.QAllowRedirectDialogs || mcConst.UserInfo.hasInvite, css: "btnNoPadding noUpperCase 1byCenter", height: 30}, // "618":"Перенаправить",

                    { view: "button", id: uAct + "BeepBtn", type: "icon", icon: "bell-o",    label: mcLang(708), click: $scope.BEEP,
                        hidden: !$scope._rights.QEnableWakeUpBeep || mcConst.UserInfo.hasInvite, css: "btnNoPadding noUpperCase 1byCenter", height: 30}, // "708":"Отправить сигнал",

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

                    { id: uAct + "ReceiveFiles", view: "checkbox", value: $scope.userConfig.autoReceive, opt: "autoReceive", height: 30, hidden: mcConst.UserInfo.hasInvite,
                        labelRight: mcLang(602), labelWidth: 0, css: "chkPadding finger font14l", click: $scope.changeOptions}, // "602":"Принимать файлы не задавая вопросов",

                    { id: uAct + "NotifyOnEnter", view: "checkbox", value: 0, opt: "notifyOnline", height: 30, hidden: mcConst.UserInfo.hasInvite,
                        labelRight: mcLang(704), labelWidth: 0, css: "chkPadding finger font14l", click: $scope.changeOptions}, // "704":"Уведомлять при входе",

                    { id: uAct + "RedirectUserWrap", hidden: true, rows: [
                        new mcService.FilterFiled({
                            listName     : uAct + "RedirectUserList",
                            css          : "noBorderAll",
                            clearID      : "RedirectUser",
                            id           : "FilterRedirectUser",
                            // noClearBtn   : true,
                            placeholder  : mcLang(48), // "48" :"Поиск...",
                            filterParams : ['DisplayName', 'UIN'],
                            keyEvents    : {
                                13: function (_item) {
                                    var item = _item || $scope.RedirectUserList.getSelectedItem() || $scope.RedirectUserList.getItem($scope.RedirectUserList.getFirstId());

                                    if (item){
                                        $scope.doRedirect(item);

                                        return true;
                                    }
                                },
                                27: function () {
                                    $scope.hideRedirectList();
                                }
                            }
                        }),

                        { id: uAct + "RedirectUserList", view: "list", css: "scrollMenu noBGColor lHeight26", hidden: mcConst.UserInfo.hasInvite, type: {height: 26},
                            template: function (obj) {
                                return obj.DisplayName + " [" + obj.UIN + "]";
                            },
                            on: {
                                onItemDblClick: function (id) {
                                    $scope.doRedirect($scope.RedirectUserList.getItem(id));
                                }
                            }
                        },
                        { view: "button", value: mcLang(33), click: function () { // "33" :"Отмена",
                            $scope.hideRedirectList();
                        }}
                    ]},

                    {gravity:0},
                ]}},

                { view: "button", value: mcLang(552), click: $scope.removeUserFromDialogs, hidden: mcConst.UserInfo.hasInvite} // "552":"Закрыть диалог",
            ]}
        ]});

        __view = $$($scope.Name);

        $scope.uPrivateInfo = $$("uPrivateInfo");
        $scope.uFoto = $$("uFoto");

        $scope.userActionsDisplayName = $$("userActionsDisplayName");

        $scope.SendDirect             = $$(uAct + "SendDirect");
        $scope.RedirectBtn            = $$(uAct + "RedirectBtn");
        $scope.chckNotifyOnEnter      = $$(uAct + "NotifyOnEnter");
        $scope.BeepBtn                = $$(uAct + "BeepBtn");

        $scope.chckReceiveFiles       = $$(uAct + "ReceiveFiles");
        $scope.RedirectUserWrap       = $$(uAct + "RedirectUserWrap");
        $scope.RedirectUserList       = $$(uAct + "RedirectUserList");
        $scope.SwitchTool             = $$(uAct + "SwitchTool");

        $scope.PrivateState            = $$("uPrivateState");
        $scope.PrivateTypingTextNotify = $$("uPrivateTypingTextNotify");

        $scope.userActions  = $$("userActions" + $scope.Name);
        $scope.userInfo     = $$("userInfo" + $scope.Name);

        $scope.miniUserPhoto = $$("miniUserPhoto");
    } else {
        __view.show();
    }

    return __view;
}
function privateInfoController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.PrivateInfo;

    // var what       = "DISPLAYNAME,SEX,UIN,EMAIL,WORK_PHONE,WORK_DIVDEPT,HOME_BIRTHDAY,WORK_POSITION,WORK_COMPANYNAME,CLIENTVERSION,ROLE";
    var view          = null;
    var currentUIN    = 0;
    var fieldInfo     = {
        UIN             : '<i class="fa fa-asterisk"></i>&nbsp;',
        EMAIL           : '<i class="fa fa-envelope-o"></i>&nbsp;',
        WORK_PHONE      : '<i class="fa fa-phone"></i>&nbsp;',
        WORK_DIVDEPT    : '<i class="fa fa-sitemap"></i>&nbsp;',
        HOME_BIRTHDAY   : '<i class="fa fa-birthday-cake"></i>&nbsp;',
        WORK_POSITION   : '<i class="fa fa-briefcase"></i>&nbsp;',
        WORK_COMPANYNAME: '<i class="fa fa-building-o"></i>&nbsp;',
        CLIENTVERSION   : '<i class="fa fa-comments"></i>&nbsp;',
        ROLE            : '<i class="fa fa-users"></i>&nbsp;',
        IGNORE          : '<i class="fa fa-ban red"></i>&nbsp;'
    };
    var sexIcon       = {
        0: "<span class='fa fa-user'></span>",
        1: "<span class='fa fa-male'></span>",
        2: "<span class='fa fa-female'></span>"
    };
    var usersChangeID = {};
    var integrations  = {
        Asterisk      : false, // включена интеграция с Asterisk
        AutoNotifiers : false  // (8.7+) автоматические "важные" уведомления (Ctrl+4)
    };

    var timerTypingNotify = null;
    var isWebSupportUser  = false;
    var penaltyBeepTimer  = null;

    $scope.PrivateTypingTextNotify = null;
    // $scope.noImage      = '<img id="uFotoCanvas" src="' + mcConst.imagesPath.all + 'noimage.png" border="0">';
    $scope.noImage      = '<img src="' + mcConst.imagesPath.all + 'noimage.png" border="0" style="max-height: 99%">';
    $scope.uPrivateInfo = null;
    $scope.uFoto        = null;
    $scope.PrivateState = null;
    $scope.userConfig   = {};
    $scope._rights      = {
        QFilesSend: false,
        QAllowViewRemoteUserProfile: false,
        QAllowRedirectDialogs: false,
        QEnableWakeUpBeep: false,
        QAllowViewRemoteUserActiveProcess: false,
        QAllowViewUserGeoPosition: false
    };

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

    function what() {
        return $rootScope.isWebClient ? mcConst.defaultUserFields : mcConst.ClientSettings.AdditionalPrivateInfoFields;
    }

    function fillUserInfo(data, uin){
        $scope.data = data;

        currentUIN = uin;

        var list = what().split(','); //$scope.data.What.split(',');
        var res = [
            mcService.myReplaceFormated(
                '<div class="byCenter bolder brown lHeight30 #{finger}" onclick="window._userActions.openUserProfile(#{uin},\'#{name}\')">#{sex}#{name}</div>',
                {
                    finger      : $scope._rights.QAllowViewRemoteUserProfile ? "finger" : "",
                    uin         : uin,
                    name        : mcService.trim($scope.data.DISPLAYNAME),
                    sex         : sexIcon[$scope.data.SEX]
                }
            ),

            "<div>" + fieldInfo.UIN + uin + "</div>"
        ];

        $scope.userActionsDisplayName.define('template', res[0]);
        $scope.userActionsDisplayName.refresh();

        $scope.data.HOME_BIRTHDAY = mcService.toNormalDate($scope.data.HOME_BIRTHDAY);

        for (var i = 0; i < list.length; i++)
            if (list[i] === "DISPLAYNAME" || list[i] === "SEX") { /*skip*/ } else
            if ( $scope.data[list[i]] && $scope.data[list[i]] !== ''){
                if ((list[i] === "HOME_BIRTHDAY" && $scope.data[list[i]] !== "01.01.1900") || (list[i] !== "HOME_BIRTHDAY")) {
                    res.push("<div>" + fieldInfo[list[i]] + $scope.data[list[i]] + "</div>");
                }
            }

        // "UINFrom"  : 65,            // идентификатор запрашиваемого пользователя, заголовок активного процесса которого нам нужно получить
        // "CIDFrom"  : 2,             // (6.7+) CID отправителя
        // "Caption"  : "Калькулятор", // текст заголовка активного процесса
        // "AppName"  : "",            // (6.7+) название активного приложения
        // "FullPath" : ""             // (6.7+) полный путь к ативному приложению

        res.push("<div><i class='fa fa-clock-o' style='margin-left: 2px'></i>&nbsp;<span id='userCurrentTime-" + uin + "'></span></div>");
        
        if ($scope._rights.QAllowViewRemoteUserActiveProcess){
            res.push("<br>");
            res.push("<div id='userActiveProcess-" + uin + "' style='font-size: 90%; word-break: break-word'>&nbsp;<i class='fa fa-refresh finger brown noPadding' uin='#{uin}' style='width: 12px' onclick='window._userActions.refreshUserProcess.apply(this, arguments)'></i></div>");
        }

        if ($scope._rights.QAllowViewUserGeoPosition && $rootScope.GetChatTypeID() !== $rootScope.chatAliases.Elisa){
            res.push("<br>");
            res.push("<a class='finger' style='font-size: 90%; word-break: break-word' uin='#{uin}' onclick='window._userActions.showUserGeolocation.apply(this, arguments)'>" +
                "<i class='fa fa-map-marker brown noPadding' style='width: 12px; margin-left: 5px'></i> " +
                mcLang(733) + // "733":"Запрос геопозиции",
            "</a>");
        }

        $scope.uPrivateInfo.setHTML(res.join(''));
    }

    function fillPrivateInfo (data, uin){
        $rootScope.$broadcast('getUserIgnoreInfo', [uin, function(ignores){
            if (ignores && ignores.Ignores[1] == '1'){
                data.What += ',IGNORE';

                data.IGNORE = mcService.Lang(50); // "50" : "В игноре",
            }
        }]);

        fillUserInfo(data, uin);

        var state = $rootScope.$broadcast(window._messages_.dialogsList.getUserState, [uin]);
        
        changeState(state);

        $rootScope.$broadcast(window._messages_.callManager.changeCallState, [state, uin]);

        $scope.userConfig = $rootScope.customUserOptions.getUser(uin);

        $scope.chckReceiveFiles .setValue($scope.userConfig.autoReceive);
        $scope.chckNotifyOnEnter.setValue($scope.userConfig.notifyOnline);
    }

    function startDrawTypingNotify(){
        if (view && view.isVisible()){
            var notifyView = mcService.getFrame('uPrivateTypingNotify');

            if (notifyView && notifyView.className.indexOf('noBG') === -1) {
                notifyView.className += ' noBG';
            }
            mcService.showFrame('animateTypingNotify');

            $scope.PrivateTypingTextNotify.define('template', mcService.Lang(49)); // "49" : "пишет...",
            $scope.PrivateTypingTextNotify.refresh();
        }
    }

    function stopDrawTypingNotify(){
        // var notifyView = mcService.getFrame('uPrivateTypingNotify');

        clearTimeout(timerTypingNotify);

        timerTypingNotify = null;

        mcService.hideFrame('animateTypingNotify');

        if (view && view.className && view.className.indexOf('noBG') !== -1) {
            view.className = view.className.replace(' noBG', "");
        }

        $scope.PrivateTypingTextNotify.define('template', ' ');
        $scope.PrivateTypingTextNotify.refresh();
    }

    function changeState(state){
        if ($scope.PrivateState){
            switch (parseInt(state)){
                case mcConst.states.offline:
                    $scope.PrivateState.define('template', mcService.Lang(29)); break; // "29" : "Не в сети",

                case mcConst.states.online:
                    $scope.PrivateState.define('template', '<span class="green">' + mcService.Lang(30) + '</span>'); break; // "30" : "В сети",

                case mcConst.states.webOnline:
                    $scope.PrivateState.define('template', '<span class="green">' + mcService.Lang(30) + '</span>'); break; // "30" : "В сети",

                case mcConst.states.away:
                    $scope.PrivateState.define('template', '<span class="blue">' + mcService.Lang(31) + '</span>'); break; // "31" : "Нет на месте",

                case mcConst.states.dnd:
                    $scope.PrivateState.define('template', '<span class="red">' + mcService.Lang(32) + '</span>'); break; // "32" : "Не беспокоить",
            }

            $scope.PrivateState.refresh();
        }
    }

    function getLocalTime(uin){
        if (uin != 0) {
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_get_remote_uin_current_time,
                mcConst.SessionID,

                uin,

                function (data) {
                    // "UINFrom"  : 89,                     // UIN удалённого пользователя
                    // "CIDFrom"  : 1234,                   // (8.2+) CID удалённого пользователя
                    // "AppType"  : "android",              // (8.2+) тип приложения пользователя
                    // "DateTime" : "17.04.2013.14.15.23"   // дата и время в формате дд.мм.гггг.чч.мм.сс

                    var userTime = document.getElementById("userCurrentTime-" + uin);

                    if (userTime) {
                        userTime.innerHTML = mcService.formatDate(data.DateTime, "hh:nn:ss");
                    }
                }
            ]);
        }
    }

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

    $scope.BEEP = function(){
        if (!penaltyBeepTimer){
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_private_msg,
                mcConst.SessionID,

                JSON.stringify({
                    UIN : $rootScope.GetChatID(),
                    Msg : "",
                    MsgType: mcConst._CMD_.msgType.BEEP
                })
            ]);

            var count  = 60;

            $scope.BeepBtn.define("label", mcLang(708) + " (" + count + ")");
            $scope.BeepBtn.refresh();
            $scope.BeepBtn.disable();

            count--;

            var _timer = setInterval(function () {
                if (!count) {
                    clearInterval(_timer);
                    _timer = null;

                    $scope.BeepBtn.define("label", mcLang(708));
                } else {
                    $scope.BeepBtn.define("label", mcLang(708) + " (" + count + ")");

                    count--;
                }

                $scope.BeepBtn.refresh();
            }, 1000);

            penaltyBeepTimer = setTimeout(function () {
                penaltyBeepTimer = null;

                $scope.BeepBtn.enable();
            }, 1000 * 60); // minute
        }
    };

    $scope.showRedirect = function(){
        isWebSupportUser = false;
        
        if ($rootScope.GetChatType() === $rootScope.chatAliases.UIN && $rootScope.GetChatID()){
            $scope.RedirectBtn.disable();
            $scope.SendDirect       .hide();
            $scope.chckNotifyOnEnter.hide();
            $scope.chckReceiveFiles .hide();
            $scope.BeepBtn          .hide();
            
            $scope.RedirectUserWrap .show();

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst.lockInterface,
                mcConst._CMD_.cs_get_users_for_redirect_dialog,
                mcConst.SessionID,
                
                $rootScope.GetChatID(),

                function (list) {
                    // "UIN"        : 8517,                    // пользователь, которого будем редиректить
                    // "WebSupport" : true,                    // true, если это список людей из websupport-а, false - если полный список людей с сервера, кроме Web Guests
                    // "Users"      :                          // список пользователей для редиректа. Массив объектов, в массиве может быть UIN отправителя запроса, его следует пропустить при рендеринге
                    // [{
                    //         "UIN"         : 17,               // UIN пользователя
                    //         "DisplayName" : "Terminator",     // отображаемое в чате имя
                    //         "Avatar"      : 21,               // аватар пользователя
                    //         "Sex"         : 1,                // пол пользователя
                    //         "State"       : 0                 // текущий статус пользователя (0 - свободен)
                    // }]
                    isWebSupportUser = list.WebSupport;

                    $scope.RedirectUserList.parse(list.Users);
                    
                    $$("FilterRedirectUserEditor").focus();
                }
            ]);
        }
    };

    $scope.hideRedirectList = function(){
        $$("clearBtnRedirectUser").callEvent("onItemClick");

        $scope.RedirectUserWrap.hide();
        $scope.RedirectUserList.clearAll();
        $scope.RedirectBtn.enable();

        $scope.SendDirect       .show();
        $scope.chckNotifyOnEnter.show();
        $scope.chckReceiveFiles .show();
        $scope.BeepBtn          .show();
    };

    $scope.doRedirect = function(userTo){
        if (userTo && $rootScope.GetChatType() === $rootScope.chatAliases.UIN){
            $scope.hideRedirectList();

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_redirect_private_talk,
                mcConst.SessionID,
                
                $rootScope.GetChatID(),
                userTo.UIN
            ]);

            if (isWebSupportUser){
                $rootScope.$broadcast(window._messages_.dialogsList.removeDialogFromList, [$rootScope.GetChatID()]);
            }
        }
    };

    $scope.changeOptions  = function() {
        var val = this.getValue();
        var opt = this.config.opt;

        if ($scope.userConfig[opt] != val && opt === "notifyOnline") {
            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_set_user_online_notify,
                mcConst.SessionID,

                currentUIN,
                !!val
            ]);
        }

        $scope.userConfig[opt] = val;

        $rootScope.customUserOptions.setUserOption(currentUIN, opt, val);
    };

    $scope.directSend     = function() {
        $rootScope.$broadcast(window._messages_.downloadUpload.sendFilesToUser);
    };

    $scope.drawFoto       = function(fotoData){
        if (fotoData && fotoData.CRC32){
            $scope.uFoto.define('template', '<img src="data:image/jpg;base64,' + fotoData.Foto +'" style="max-height: 100%" >');
            $scope.uFoto.refresh();

            $scope.miniUserPhoto.define('template', '<img src="data:image/jpg;base64,' + fotoData.Foto +'" style="max-height: 99%" >');
            $scope.miniUserPhoto.refresh();
        } else {
            $scope.miniUserPhoto.define('template', $scope.noImage);
            $scope.miniUserPhoto.refresh();
        }
    };

    $scope.changeView     = function (newView) {
        if (newView === '1'){
            $scope.userInfo.hide();
            $scope.userActions.show();
        } else {
            $scope.userActions.hide();
            $scope.userInfo.show()
        }
    };

    $scope.removeUserFromDialogs = function () {
        $rootScope.$broadcast('removeDialogFromList', [$rootScope.GetChatID()]);
    };

    $scope.getData        = function(uin){
        if (currentUIN != uin){

            $rootScope.$broadcast(window._messages_.dialogsList.getUserProfile, [uin, usersChangeID[uin], function (user) {
                fillPrivateInfo(user, uin);
                
                $scope.drawFoto(user.Photo);

                usersChangeID[uin] = user.ChangeID;

                setTimeout(function (){
                    $rootScope.$broadcast(window._messages_.dialogsList.getActiveProcess, [uin]);
                }, 150);

                getLocalTime(uin);
            }]);

            $rootScope.setTool($scope.Name);
        }
    };

    $scope.show           = function(uin){
        view = initPrivateInfo($scope);

        if (currentUIN != uin){
            $scope.uPrivateInfo.define('template', "");
            $scope.uPrivateInfo.refresh();

            $scope.uFoto.define('template', ($rootScope.needFoto == 0) ? $scope.noImage : ' ');
            $scope.uFoto.refresh();
        }
    };

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

    var _msg = window._messages_.privateInfo = {
        addIgnoreInPrivateInfo      : "addIgnoreInPrivateInfo",
        removeIgnoreInPrivateInfo   : "removeIgnoreInPrivateInfo",
        // fillPrivateInfo             : "fillPrivateInfo",
        onlineOfflineUsers          : "onlineOfflineUsers",
        onTypingNotify              : "onTypingNotify",
        redirectUserTo              : "redirectUserTo",
        setChangeIdForUser          : "setChangeIdForUser",
        updateUserActiveProcess     : "updateUserActiveProcess",
        changeStateForUser          : "changeStateForUser",
        on_sc_integrations          : "on_sc_integrations"
    };

    $scope.$on(_msg.on_sc_integrations, function (e, args){
        integrations = args[0];
    });

    $scope.$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
        }]);
    });

    $scope.$on(_msg.addIgnoreInPrivateInfo, function(e, args){
        var uin = args[0];

        if (uin == currentUIN){
            $scope.data.What += ',IGNORE';
            $scope.data.IGNORE = mcService.Lang(50); // "50" : "В игноре",

            fillUserInfo($scope.data, currentUIN);
        }
    });

    $scope.$on(_msg.updateUserActiveProcess, function(e, args){
        if ($scope._rights.QAllowViewRemoteUserActiveProcess) {
            (function (uin, info) {
                setTimeout(function () {
                    var elem = document.getElementById("userActiveProcess-" + uin);

                    if (elem && info) {
                        var appName = info.AppName ? (info.AppName.split('.')).slice(0, -1) : "";

                        elem.innerHTML = (mcService.myReplaceFormated(
                            "<span>&nbsp;<i class='fa fa-refresh finger brown noPadding' uin='#{uin}' style='width: 12px' onclick='window._userActions.refreshUserProcess.apply(this, arguments)'></i></span> <span style='font-weight: bolder; color:#121212'>#{app}</span>: #{caption}",
                            {
                                uin: uin,
                                app: appName,
                                caption: info.Caption
                            }
                        ));
                    }
                }, 600)
            }).apply(null, args);
        }
    });

    $scope.$on(_msg.removeIgnoreInPrivateInfo, function(e, args){
        var uin = args[0];

        if (uin == currentUIN){
            $scope.data.What = $scope.data.What.toString().replace(',IGNORE', '');

            if ($scope.data.IGNORE) delete $scope.data.IGNORE;

            fillUserInfo($scope.data, currentUIN);
        }
    });

    $scope.$on(_msg.changeStateForUser, function(e, args){
        var UIN   = args[0];
        var onlineState = args[1];

        if (UIN == currentUIN) {
            changeState(onlineState);
        }
    });

    $scope.$on(_msg.onlineOfflineUsers, function(e, args){
        var Statuses = mcService.getStateStatuses(args[0]);

        if (mcService.inArrayNoStrict(currentUIN, Statuses.Online) >= 0){
            changeState(mcConst.states.online);
        } else
        if (mcService.inArrayNoStrict(currentUIN, Statuses.Offline) >= 0){
            changeState(mcConst.states.offline);
        }
    });

    $scope.$on(_msg.onTypingNotify, function(e, args){
        var uin = args[0].UIN;

        if (uin == currentUIN){
            if (!timerTypingNotify){
                startDrawTypingNotify();

                timerTypingNotify = setTimeout(function(){
                    stopDrawTypingNotify();
                }, 4000);
            }
        }
    });

    $scope.$on(_msg.redirectUserTo, function(){
        if ($rootScope.GetChatType() === $rootScope.chatAliases.UIN && $rootScope.GetChatID){
            $rootScope.$broadcast("selectTool", [mcConst.dataModels.PrivateInfo, !$rootScope.$broadcast(window._messages_.chatWrapper.isVisibleRightSide)]);

            $scope.SwitchTool.setValue('1');

            $scope.RedirectBtn.callEvent("onItemClick");
        }
    });

    $scope.$on('hide' + $scope.Name, function(){
        view.hide();
        currentUIN = -1;
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];
        $scope.isWeb = $rootScope.isWebClient;

        var uin = $rootScope.GetChatID();
        var isPrivate = $rootScope.GetChatType() === $rootScope.chatAliases.UIN;

        if (isPrivate && (uin != currentUIN || $rootScope.getTool() != $scope.Name)) {
            $scope.show(uin);
            $scope.getData(uin);

            // if (mcMedia.nowCalling()){
            //     $scope.showStopCall();
            // }
        } else
        if (uin == currentUIN && $scope.data && $scope.data.Photo) {
            $scope.drawFoto($scope.data.Photo);

            getLocalTime(uin);
        }
    });

    $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
        $scope._rights.QFilesSend                   = mcService.checkRights(mcConst._CMD_.RS.QFilesSend);
        $scope._rights.QAllowViewRemoteUserProfile  = mcService.checkRights(mcConst._CMD_.RS.QAllowViewRemoteUserProfile);
        $scope._rights.QAllowRedirectDialogs        = mcService.checkRights(mcConst._CMD_.RS.QAllowRedirectDialogs);
        $scope._rights.QEnableWakeUpBeep            = mcService.checkRights(mcConst._CMD_.RS.QEnableWakeUpBeep);
        $scope._rights.QAllowViewUserGeoPosition    = mcService.checkRights(mcConst._CMD_.RS.QAllowViewUserGeoPosition);
        $scope._rights.QAllowViewRemoteUserActiveProcess = mcService.checkRights(mcConst._CMD_.RS.QAllowViewRemoteUserActiveProcess);

        if ($scope.SendDirect && !$scope.isWeb){
            $scope.SendDirect[$scope._rights.QFilesSend ? 'show' : 'hide']();
        }

        $scope.RedirectBtn && $scope.RedirectBtn[$scope._rights.QAllowRedirectDialogs ? 'show' : 'hide']();
    });
}

function initFtpFiles($scope){
    "use strict";

    var view = $$($scope.Name);

    if (!view) {
        $$($scope.container).addView({
            id   : $scope.Name,
            rows : [
                { cols: [
                    { view: "label", label: mcLang(658), align: 'center'}, // "658":"FTP файлы",

                    { view:"button", type:"icon", id: "folder" + $scope.Name, icon: "folder", width: 35, css: "icoButton button_primary byCenter iconNoPadding", 
                        click: $scope.openDownloadFolder, tooltip: mcLang(684)}, //  "684":"Показать папку с файлами",

                    { view:"toggle", type:"iconButton", id: "pin" + $scope.Name, offIcon: "nezamok", onIcon: "lock", width: 30, css: "btnPaddingToggle4", paddingX: 3, on:{
                        onChange: $scope.pinTool
                    }, tooltip: mcLang(511)} //  "511":"Закрепить/открепить",
                ]},

                $scope.dwlMngr.progressWrapper,

                {cols: [
                    mcService.FilterFiled({
                        listName     : 'ftpList' + $scope.Name,
                        placeholder  : mcLang(48), // "48" :"Поиск...",
                        css          : "noBorder",
                        filterParams : ['name'],
                        id           : "ftpListFilterField"
                    }),
                    { width: 4}
                ]},

                { height: 5 },
                
                {
                    view          : "treetable",
                    resizeColumn  : true,
                    id            : "ftpList" + $scope.Name,
                    select        : "row",
                    header        : false,
                    multiselect   : true,
                    navigation    : false,
                    scroll        : "y",
                    css           : "noBG noBorderAll noWhiteSpace scrollMenu noPaddingCell",
                    fixedRowHeight: true, //false,
                    rowLineHeight :25,
                    rowHeight     :25,
                    columns       : [
                        { id: "Name", fillspace: true, css: "font85 ellipsisTextAll", template: function (obj, common) {
                            var name  = "";
                            var title = "";

                            if (obj.name === "..") {
                                name = mcService.insertIco("fa-level-up", "smaller flipped") + " <span class='treeBranchContacts webix_tree_item'>" + mcLang(63) + "</span>"; // "63" :"Назад",
                            } else
                            if (obj.folder) {
                                name = mcService.insertIco("fa-folder", "smaller") + "<span class='treeBranchContacts webix_tree_item' style='color:#00720d;'>" + obj.name + "</span>";
                            } else {
                                name  = mcService.insertIco("fa-file-o", "smaller") + obj.name;
                                title = obj.name + ", " + mcService.formatFileSize(obj.size);
                            }

                            return common.space(obj) + "<div class='webix_tree_item lHeight26' style='cursor: pointer;' title='" + title + "'>" + name + "</div>";
                        }},
                        
                        { id: "size", css: "font80 byRight showOnOver", width: 60, template: function (obj) {
                            var size = parseInt(obj.size);
                            var id   = obj.row;
                            var res  = "";

                            if (size || (!size && !obj.folder)) {
                                res = mcService.myReplaceFormated(
                                    "<span class='needShow finger _ftpDownload' title='#{dwTitle}' style='width: 100%; text-align: center;'>#{dw}</span><span class='needHide'>#{size}</span>",
                                    {
                                        dwTitle: mcLang(573), // "573":"Скачать",
                                        id  : id,
                                        dw  : mcService.insertIco("fa-download"),
                                        size: mcService.formatFileSize(obj.size)
                                    }
                                );
                            } else

                            if (obj.ext) {
                                res = mcService.insertIco("fa-share-square-o", "finger openExternal-" + obj.type, "title='" + mcLang(705) + "'"); // "705":"Открыть в файловом менеджере",
                            }

                            return res;
                        }}
                    ],
                    onClick: $scope.downloadLink,
                    on: {
                        onItemDblClick: function (data) {
                            var id   = data.row;
                            var item = $scope.fileList.getItem(id);

                            $scope.getPath(item.name, !item.folder, item.type, item.size);
                        }
                    }
                }
            ]
        });

        view = $$($scope.Name);

        $scope.fileList = $$("ftpList" + $scope.Name);

        // $scope.initDU();
    } else {
        view.show();

        $$("pin" + $scope.Name).setValue(false);
    }

    return view;
}
function ftpFilesController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.CommonFiles;

    var frameView   = null;
    var currentPath = ".";
    var ftpConnect  = null;
    var PUBLIC      = "public";
    var PRIVATE     = "private";
    var ROOT        = "root";
    var ftpType     = ROOT;
    var downloadUploadManager = null;
    var getPathProcess        = false;

    $scope._rights = {
        QFTPPublic: false,
        QFTPPersonal: false
    };

    $rootScope.ElectronErrors[mcConst._CMD_.errElectron.eFtpError] = function (err) {
        console.error(err);

        ftpConnect = null;
        ftpType    = ROOT;
    };

    $rootScope.ElectronErrors[mcConst._CMD_.errElectron.eFtpNotConnected] = function () {
    };

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

    function setFtpUrl(url) {
        if (frameView && url.indexOf(mcService.getLocalHostPath($rootScope.isWebClient) === 0)){
            frameView.define("src", url);
        } else {
            console.error("Wrong URL for iFrame:" + url);
        }
    }

    function convertStructToTree(data){
        // name: 'README.txt',
        // type: 0,
        // time: 996052680000,
        // size: '2582',
        // owner: 'sergi',
        // group: 'staff',
        // userPermissions: { read: true, write: true, exec: false },
        // groupPermissions: { read: true, write: false, exec: false },
        // otherPermissions: { read: true, write: false, exec: false }

        var res = [];

        if (data && data.length){
            data.forEach(function (item) {
                res.push({
                    folder: !!item.type,
                    name  : item.name,
                    size  : item.size,
                    time  : item.time,
                    prev  : ""
                });
            })
        }

        return res;
    }
    
    function setLoading() {
        getPathProcess = true;
        
        $scope.fileList.getNode().className += " loadingCursor";
    }
    
    function removeLoading() {
        getPathProcess = false;

        $scope.fileList.getNode().className = $scope.fileList.getNode().className.replace(" loadingCursor", "");
    }

    function getFtpPath(path, isFile, type, size){
        if (getPathProcess) {
            return;
        }
        
        if (type === PUBLIC || type === PRIVATE) {
            currentPath = "/";
        } else {
            if (path === "..") {
                if (!currentPath || currentPath === "/") {
                    currentPath = ".";
                } else {
                    currentPath = currentPath.split("/");
                    currentPath.pop();
                    currentPath = currentPath.join("/");
                }
            } else
            if (!isFile && path !== "/" && path !== void(0)) {
                if (currentPath === "/")
                    currentPath += path;
                else
                    currentPath += "/" + path;
            }
        }

        if (type){
            $scope.getData(type);
        } else
        if (isFile) {
            $rootScope.$broadcast(window._messages_.downloadUpload.download_ftp_file, [
                currentPath + (currentPath !== "/" ? "/" : "") + path,
                path, // if isFile === true in path = file name
                size
            ]);
        } else      
        if (currentPath === ".") {
            $scope.fileList.clearAll();
            $scope.fileList.parse([]
                .concat($scope._rights.QFTPPublic ? [{
                    folder: true,
                    name  : PUBLIC,
                    type  : PUBLIC,
                    ext   : true
                }] : [])
                .concat($scope._rights.QFTPPersonal ? [{
                    folder: true,
                    name  : PRIVATE,
                    type  : PRIVATE,
                    ext   : true
                }] : [])
            );

            if (ftpConnect) {
                $rootScope.$broadcast("sendCMDToElectron", [
                    mcConst._CMD_.ce_ftp_quit
                ]);

                ftpConnect = null;
            }
        } else {
            currentPath = !currentPath ? "/" : currentPath;

            setLoading();

            setTimeout(function() {
                $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                    mcConst._CMD_.ce_ftp_list,
                    currentPath,

                    function (data) {
                        var list = convertStructToTree(data);

                        list.sort(function (a, b) {
                            return mcService.sortAB(b.folder, a.folder);
                        });

                        if (currentPath !== ".") {
                            list.unshift({
                                folder: true,
                                name: "..",
                                size: 0,
                                time: 0,
                                prev: path
                            });
                        }

                        $scope.fileList.clearAll();
                        $scope.fileList.parse(list);

                        removeLoading();
                    }
                ]);
            }, 10);
        }
    }

    function openItem(data){
        var id   = data.row;
        var item = $scope.fileList.getItem(id);

        getFtpPath(item.name, !item.folder, item.type, item.size);
    }

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

    $scope.downloadLink = {
        "openExternal-private": function(){
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_create_token,
                mcConst.SessionID,

                "ftp_personal_token_" + mcConst.UserInfo.UIN,
                ""
           ]);
        },
        
        "openExternal-public": function(){
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_create_token,
                mcConst.SessionID,

                "ftp_public_token_" + mcConst.UserInfo.UIN,
                ""
            ]);
        },

        "_ftpDownload": function(e, data){
            openItem(data);

            return false; // here it blocks the default behavior
        }
    };

    $scope.openDownloadFolder = function(){
        if (mcConst.currentDownloadFolder) {
            downloadUploadManager.openFolder(mcConst.currentDownloadFolder);
        }
    };

    $scope.pinTool = function(on){
        $rootScope.$broadcast( on ? 'pinTool' : 'unPinTool', [$scope.Name]);
    };

    $scope.getData = function(type){
        if ((!type || !ftpConnect) && (type && type !== ROOT && currentPath !== ".")) {
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_create_token,
                mcConst.SessionID,

                (type === PUBLIC ? "ftp_public_token_" : "ftp_personal_token_") + mcConst.UserInfo.UIN,
                "",

                function (data) {
                    ftpConnect = mcService.StringToObj(data);
                    ftpType    = type;

                    $rootScope.$broadcast("sendCMDToElectron", [
                        mcConst._CMD_.ce_ftp_login,
                        ftpConnect.Where,
                        ftpConnect.Token,

                        function () {
                            getFtpPath();
                        }
                    ]);
                }
            ]);
        } else {
            getFtpPath();
        }
    };

    $scope.show    = function(){
        $rootScope.setTool($scope.Name);

        if (!frameView){
            downloadUploadManager = new DownloadUploadManager($rootScope, {
                name: $scope.Name,
                download  : {
                    enable : true
                }
            });

            // $scope.uploadFile = downloadUploadManager.uploadFile;
            $scope.initDU     = downloadUploadManager.initViews;
            $scope.dwlMngr    = downloadUploadManager.getViews(mcLang(33)); // "33" :"Отмена",
        }

        frameView = initFtpFiles($scope);
    };

    $scope.getPath = getFtpPath;

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

    var _msg = window._messages_.commonFiles = {
        openCommonFilesUrl  : 'openCommonFilesUrl',
        ftpError            : 'ftpErrorElectron'
    };

    $scope.$on(_msg.openCommonFilesUrl, function (e, args) {
        setFtpUrl.apply(null, args);
    });

    $scope.$on(_msg.ftpError, function (e, args) {
        console.warning("FTP error: " + args[0]);

        ftpConnect = null;

        removeLoading();
    });

    $scope.$on('hide' + $scope.Name, function(){
        $rootScope.$broadcast("sendCMDToElectron", [
            mcConst._CMD_.ce_ftp_quit
        ]);

        ftpConnect = null;

        removeLoading();

        frameView.hide();
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        $scope.pathFTP = mcService.getLocalHostPath(true);

        $scope.show();
        $scope.getData(ftpType);
    });

    $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
        $scope._rights.QFTPPublic   = mcService.checkRights(mcConst._CMD_.RS.QFTPPublic);
        $scope._rights.QFTPPersonal = mcService.checkRights(mcConst._CMD_.RS.QFTPPersonal);
    });
}

"use strict";

function initHistoryDialogs($scope){
    var __view = $$($scope.Name);

    if (!__view){
        var filter = new mcService.FilterFiled({
            listName     : "historyList" + $scope.Name,
            css          : "noBorderAll",
            noClearBtn   : true,
            placeholder  : mcLang(48), // "48" :"Поиск...",
            filterParams : ['DisplayName']
        });

        $$($scope.container).addView({ id: $scope.Name, rows: [
            { cols: [
                { view: "label", label: mcLang(578), align: 'center'}, //  "578":"История диалогов",

                { view:"toggle", type:"iconButton", id: "pin" + $scope.Name, offIcon: "nezamok", onIcon: "lock", width: 30, css: "btnPaddingToggle4", paddingX: 3, on:{
                    onChange: $scope.pinTool
                }, tooltip: mcLang(511)} //  "511":"Закрепить/открепить",
            ]},

            filter,

            { view: "unitlist", css: "lineHeight24 noBGColor dialogs_list" + (!isMobile? " scrollMenuYX": ""),
                scroll: false,
                id: "historyList" + $scope.Name,
                uniteBy: function(obj){
                    return obj.date;
                },
                type: {
                    headerHeight:25,
                    height: 25
                },
                scheme:{
                    $sort: function(a, b){
                        return mcService.sortAB(b.sortDate, a.sortDate);
                    }
                },
                template: function (obj) {
                    return "<span class='historyItem nowrap'>" + obj.DisplayName + "</span><span class='messageKanbanTime toRight'>(" + obj.time + ")</span>";
                },
                data: $scope.data,
                on: { onItemClick: $scope.openPrivate }
            }
        ]});

        __view = $$($scope.Name);

        $scope.historyList = $$("historyList" + $scope.Name);
    } else {
        __view.show();

        $$("pin" + $scope.Name).setValue(false);
    }

    return __view;
}

"use strict";

function historyDialogsController($scope, $rootScope){
    $scope.Name = mcConst.dataModels.HistoryDialogs;

    var _view = null;

    $scope.data = [];

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

    function historyUser(_user) {
        var dt = _user.dtLastMessage.split('.');

        if (dt){
            _user.date = mcService.myReplaceFormated(
                "#{day} #{month}, #{year}",
                {
                    day  : dt[0],
                    month: webix.i18n.calendar.monthShort[dt[1] - 1],
                    year : dt[2]
                }
            );

            _user.time = mcService.myReplaceFormated(
                "#{hour}:#{min}:#{sec}",
                {
                    hour: dt[3],
                    min : dt[4],
                    sec : dt[5]
                }
            );
        }

        _user.sortDate = mcService.sortableDate(_user.dtLastMessage);

        return _user;
    }

    function fillDialogsHistory(data){
        $scope.data = mcService.convertObjToArray(data || $scope.data);

        $scope.data.forEach(function (val) {
            historyUser(val);

            val.id = "hs_" + val.UIN;
        });

        if ($scope.historyList){
            $scope.historyList.clearAll();
            $scope.historyList.parse($scope.data);
        }
    }

    function addOrMove(user) {
        if (user && user.UIN !== undefined){
            var __id = "hs_" + user.UIN;

            user.id = __id;

            if ($scope.historyList){
                var item = $scope.historyList.getItem(__id);

                if (item) {
                    item = mcService.Marge(item, historyUser(user));

                    $scope.historyList.updateItem(__id, item);
                    $scope.historyList.moveTop(__id);
                } else {
                    $scope.historyList.add(historyUser(user), 0);
                }
            } else {
                var idx = mcService.findItemInArrayOfObj($scope.data, user.UIN, "UIN");

                if (idx === -1){
                    $scope.data.unshift(historyUser(user));
                } else {
                    $scope.data[idx] = historyUser(user);
                }
            }
        }
    }

    function removeUser(uin) {
        // todo: если нужно добавить возможность удалять пользователей из истории
    }

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

    $scope.openPrivate = function(id){
        var user = $scope.historyList.getItem(id);

        if (user) {
            $rootScope.$broadcast('OpenPrivate', [user.UIN]);
        }
    };

    $scope.pinTool = function(on){
        $rootScope.$broadcast( on ? 'pinTool' : 'unPinTool', [$scope.Name]);
    };

    $scope.show = function(){
        $rootScope.setTool($scope.Name);

        _view = initHistoryDialogs($scope);
    };

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

    var _msg = window._messages_.historyDialogs = {
        dListAddOrModifyUser    : 'dListAddOrModifyUser',
        dListFillHistory        : 'dListFillHistory',
        dListHistoryRemoveUser  : 'dListHistoryRemoveUser',
    };

    $scope.$on(_msg.dListHistoryRemoveUser, function (e, args) {
        removeUser.apply(null, args);
    });

    $scope.$on(_msg.dListAddOrModifyUser, function (e, args) {
        addOrMove.apply(null, args);
    });

    $scope.$on(_msg.dListFillHistory, function (e, args) {
        fillDialogsHistory.apply(null, args);
    });

    $scope.$on('hide' + $scope.Name, function(){
        _view.hide();
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        $scope.show();
    });
}

function initDialogs($scope){
    "use strict";
  
    var __view = $$($scope.Name);
    var hHeight = 24;

    if (!__view){
        var tools    = {
            id        : "arcToolsList" + $scope.Name,
            view      : "accordionitem",
            css       : "lineHeight24 accordionListHeader",
            borderless     : true,
            headerHeight   : hHeight,
            headerAltHeight: hHeight,
            hidden    : true,
            collapsed : true,
            header    : mcLang(541), // "541":"Инструменты",
            body      : {
                id      : "toolsList" + $scope.Name,
                view    : "list",
                css     : "noBGColor listColorWhite lineHeight24 scrollMenu",
                borderless: true,
                select  : true,
                scroll  : "y",
                height  : $scope.__toolsList.length * 25,
                data    : $scope.__toolsList,
                template: $scope.tlView,
                type    : {
                    headerHeight:30,
                    height: 24
                },
                on: {
                    onAfterSelect: $scope.selectTools
                }
            }
        };

        var channels = {
            id        : "arcChannelsList" + $scope.Name,
            view      : "accordionitem",
            css       : "lineHeight24 accordionListHeader",
            // minHeight : !$scope._rights.QAllowBrowseTextConfList && !$scope._rights.QTxtChCreate ? 30 : 90,
            minHeight : 30,
            borderless: true,
            headerHeight: hHeight,
            headerAltHeight: hHeight,
            autoheight: true,
            collapsed : true,
            hidden    : !$scope._rights.QTxtChJoin && !$scope._rights.QTxtChCreate && mcConst.UserInfo.hasInvite,
            header    : mcLang(24) + "<span id='_confBadge' class='badge_tool toLeft hidden'></span>", // "24" : "Конференции",
            body      : { rows: [
                {
                    id      : "channelsList" + $scope.Name,
                    view    : "list",
                    css     : "noBGColor listColorWhite lineHeight24 hiddenScrollMenu thinScrollBar relative_list", // scrollMenu hiddenScrollMenu thinScrollBar
                    borderless: true,
                    select  : true,
                    scroll  : true,
                    type    : {
                        headerHeight: 30,
                        height      : 24
                    },
                    template: $scope.chView,
                    on: {
                        onItemClick  : $scope.clickOnDialogItem,
                        onAfterSelect: $scope.openChannelDialog
                    }
                },

                { id: "channelBtnWrapper", css: "darkGrayBorderTop", hidden: !$scope._rights.QAllowBrowseTextConfList && !$scope._rights.QTxtChCreate, cols: [
                    { id: "channelManager", view: "button", type: "icon", icon: "search", css: "imgBtn button_warning icoButton noBorderAll button_transparent ", height: 30,
                        hidden: !$scope._rights.QAllowBrowseTextConfList, tooltip: mcLang(687), // "687":"Найти конференцию",
                        click : $scope.openChannelsManager, label : mcLang(707)}, // "707":"Найти",

                    { id: "createChannel", view: "button",  type: "icon", icon: "plus", css: "imgBtn button_warning icoButton noBorderAll button_transparent ", height: 30,
                        hidden: !$scope._rights.QTxtChCreate, tooltip: mcLang(25),
                        click : $scope.openCreateConf, label : mcLang(25)}  // "25" : "Создать конференцию...",
                ]}
            ]},
            on: {
                onViewResize: function () {
                    $scope.sizes.conf = this.getNode().offsetHeight;

                    $scope.saveOptions();

                    $scope.arcDialogsList.refresh();
                }
            }
        };

        var contacts = {
            id        : "arcDialogsList" + $scope.Name,
            view      : "accordionitem",
            css       : "lineHeight20 accordionListHeader",
            header    : mcLang(20) + "<span id='_privBadge' class='badge_tool toLeft hidden'></span>", // "20" : "Мои диалоги",
            minHeight : 100,
            borderless: true,
            hidden    : mcConst.UserInfo.hasInvite,
            headerHeight: hHeight,
            headerAltHeight: hHeight,
            body      : { rows: [{
                id      : "dialogsList" + $scope.Name,
                view    : "list",
                data    : $scope.data || [],
                css     : "noBGColor listColorWhite lineHeight18 dialogs_list hiddenScrollMenu thinScrollBar", // scrollMenu
                borderless: true,
                select  : true,
                drag    : "source",
                template: $scope.cnView,
                scroll  : "y",
                type    : {
                    headerHeight:30,
                    height: 44
                },
                onContext: {},
                on: {
                    onItemClick   : $scope.clickOnDialogItem,
                    onAfterSelect : $scope.openPrivateDialog,
                    onBeforeSelect: $scope.beforeSelect,
                    onItemDblClick: $scope.removePrivateDialog,
                    onBeforeDrag  : $scope.checkDrag
                }
            }, { height: 10}]}
        };

        $$($scope.container).addView({
            id  : $scope.Name,
            minHeight: 80,
            rows: [
            { id: "_accor1",
                view      : "accordion",
                multi     : true,
                borderless: true,
                rows      : [
                    tools
                ],
                on: {
                    onViewResize: function () {
                        $scope.sizes.tool = this.$getSize()[3];

                        $scope.saveOptions();
                    }
                }
            },

            // { id: "resizer_1" + $scope.Name, view: "resizer", borderless: true, opened: {}},

            // { css: "dialogsHeaderLight", height: 24, cols: [
            //     { view: "label", label: mcLang(24), css: "byCenter"}, // "24" : "Конференции",
            //     { view: "label", label: mcLang(20), css: "byCenter"}  // "20" : "Мои диалоги",
            // ]},
            //
            //     {height: 5},

            { id: "_accor2",
                view      : "accordion",
                multi     : true,
                borderless: true,
                rows      :[
                    channels,

                    { id: "resizer_2" + $scope.Name, view: "resizer", borderless: true, opened: {}},

                    contacts
                ]
            }]
        });

        __view = $$($scope.Name);

        $scope.dialogsList     = $$("dialogsList" + $scope.Name);
        $scope.arcDialogsList  = $$("arcDialogsList" + $scope.Name);

        $scope.channelBtnWrapper = $$("channelBtnWrapper");
        $scope.channelManager    = $$("channelManager");
        $scope.createChannel     = $$("createChannel");

        $scope.toolsList       = $$("toolsList" + $scope.Name);
        $scope.arcToolsList    = $$("arcToolsList" + $scope.Name);

        $scope.channelsList    = $$("channelsList" + $scope.Name);
        $scope.arcChannelsList = $$("arcChannelsList" + $scope.Name);

        $scope.resizer_2       = $$("resizer_2" + $scope.Name);

        $scope._accor1         = $$("_accor1");
        $scope._accor2         = $$("_accor2");

        $scope._accor1.attachEvent("onAfterExpand", function (id) {
            $scope.sizes.open.tool = true;

            // hideShowResizer(id, check1Expand, check2Expand);

            $scope.saveOptions();
        });

        $scope._accor1.attachEvent("onAfterCollapse", function (id) {
            $scope.sizes.open.tool = false;

            // hideShowResizer(id, check1Collapse, check2Collapse);

            $scope.saveOptions();
        });

        $scope._accor2.attachEvent("onAfterExpand", function (id) {
            switch (id){
                case "arcChannelsList" + $scope.Name:
                    $scope.sizes.open.conf = true;
                break;

                case "arcDialogsList" + $scope.Name:
                    $scope.sizes.open.priv = true;
                break;
            }

            // hideShowResizer(id, check1Expand, check2Expand);

            $scope.saveOptions();
        });

        $scope._accor2.attachEvent("onAfterCollapse", function (id) {
            switch (id){
                case "arcChannelsList" + $scope.Name:
                    $scope.sizes.open.conf = false;
                break;

                case "arcDialogsList" + $scope.Name:
                    $scope.sizes.open.priv = false;
                break;
            }

            // hideShowResizer(id, check1Collapse, check2Collapse);

            $scope.saveOptions();
        });

        mcService.disableContextMenu(__view);

        $scope.contextDialogList = webix.ui({
            view   : "context",
            width  : 250,
            master : $scope.dialogsList,
            id     : "contextDialogList" + $scope.Name,
            css    : "noUpperCase",
            padding: 0,
            height : 60,
            body   : { padding: 0, rows: [
                { id: "inviteButtonMenu", view: "button", type: "icon", icon: "plus", css: "icoButton", height: 27, label: mcLang(714), // "714":"Пригласить в конференцию",
                    hidden: true,
                    click: function(id, e){ 
                        var context = this.getTopParentView();
                        var uin     = context.getContext().id;

                        if (uin){
                            e.preventDefault();

                            $scope.fillConfList(uin);
                            $scope.confToInvite.show(this.getNode(), {pos: "right", y: -5});
                        }
                    }
                },
                { view: "button", type: "icon", icon: "sign-out", css: "icoButton", height: 27, label: mcLang(552), click: function(id, e){ // "552":"Закрыть диалог",
                    var context = this.getTopParentView();
                    var uin     = context.getContext().id;

                    if (uin){
                        e.preventDefault();

                        $scope.removePrivateDialog(uin, "rightClick");

                        context.hide();
                    }
                }},
            ]},
            on: {
                onShow: function(){
                    if (mcConst.UserInfo.hasInvite){
                        this.hide();
                    } else {
                        var context = this.getContext();

                        var uu = context.obj.getItemNode(context.id);

                        this.blockEvent();

                        this.show(uu, {y: 12}, "bottom");

                        this.unblockEvent();
                    }
                },
                onBeforeShow: function (e) {
                    if (e.path) {
                        for (var i = 0; i < e.path.length - 1; i++){
                            if (e.path[i].className && e.path[i].className.indexOf("__dialog__item__") >= 0) {
                                this.uin    = e.path[i].getAttribute("uin");
                                this.webSup = e.path[i].getAttribute("webSup") === "true";

                                break;
                            }
                        }
                    }

                    $scope.inviteButtonMenu[!$scope._rights.QAllowInvites || this.webSup || this.uin === "0" ? "hide" : "show"]();
                }
            }
        });

        $scope.inviteButtonMenu = $$("inviteButtonMenu");

        $scope.confToInvite = webix.ui({
            view     :"submenu",
            id       :"confToInviteSubMenu",
            padding  : 0,
            autowidth: true,
            template : function(obj){
                return "<div style='margin-left: -5px; margin-right: 0; position: relative'>" +
                    (obj.userCount ? "<span class='userCount' style='color: gray'>(" + obj.userCount + ")</span> " : "") + (obj.Name || obj.ConfName) +
                "</div>";
            },
            type     : { height: 26 },
            on       : { onItemClick: $scope.inviteToConf }
        });
    } else {
        __view.show();
    }

    // hideShowResizer("arcDialogsList" + $scope.Name, check1Expand, check2Expand);
    // hideShowResizer("arcChannelsList" + $scope.Name, check1Expand, check2Expand);

    return __view;
}
function dialogsController($scope, $rootScope, mcPlaySound){
    "use strict";

    $scope.Name = mcConst.dataModels.Dialogs;
    $scope.data = [];

    var contacts       = null;
    // var confList       = {};
    // var privatList     = {};
    var privateSequence= [];
    var prevID         = 0;
    var userStatuses   = null;
    var toolSuffix     = "-tool";
    var saveTimerOption= null;

    var tabList        = [];
    var tabSelect      = null;
    var tabTimer       = null;
    var _dragItems     = false;

    $scope.__toolsList = [];
    $scope._rights     = {
        QMessengerStyle : false,
        QKanbanAccess   : false,
        QWEBAllowAdmin  : false,
        QForumAccess    : false,
        QBBSView        : false,
        QBroadcasts     : false,
        QTxtChJoin      : false,
        QTxtChCreate    : false,
        QAllowInvites   : false,
        QForciblyInvites: false,
        QAllowBrowseTextConfList: false
    };
    $scope.sizes       = {
        conf : 120,
        tool : 120,

        open: {
            conf: true,
            tool: false,
            priv: true
        }
    };

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

    window._userActions.openUserPrivate = function () {
        var uin = this.getAttribute('uin');

        if (uin != mcConst.UserInfo.UIN){
            $rootScope.$broadcast(window._messages_.dialogsList.OpenPrivate, [uin]);
        }
    };

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

    var Badges = new function () {
        var list = {};
        var _confBadge = null;
        var _privBadge = null;

        function _countConf() {
            var res = 0;

            Object.keys(list).forEach(function (item) {
                if (item.indexOf("dUID") === 0 && list[item]) {
                    res ++;
                }
            });

            if (res) {
                _confBadge = document.getElementById('_confBadge');
            }

            return res;
        }

        function _countPriv() {
            var res = 0;

            Object.keys(list).forEach(function (item) {
                if (item.indexOf("dUIN") === 0 && list[item]) {
                    res ++;
                }
            });

            if (res) {
                _privBadge = document.getElementById('_privBadge');
            }

            return res;
        }

        this.getBadge = function (obj) {
            var selectedID = $scope.channelsList.getSelectedId();

            list[obj.id] = obj.Badge;

            mcService[_countConf() ? "showView" : "hideView"](_confBadge);
            mcService[_countPriv() ? "showView" : "hideView"](_privBadge);

            if (!$rootScope.isWebClient){
                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_set_badge_count,
                    _countConf() + _countPriv()
                ]);
            }

            $rootScope.$broadcast(window._messages_.chatFrame.setUniBadge, [_countPriv() || _countPriv()]);

            return mcService.myReplaceFormated(
                "<span class='webixtype_base webix_badge' id='badge#{id}' style='#{show};'>#{value}</span>",
                {
                    id   : obj.id,
                    show : "display:" + (!obj.Badge || selectedID === obj.id ? "none" : "block"),
                    value: obj.Badge
                }
            );
        };

        this.update = function updateBadge(view, count){
            if (!count){
                mcService.hideFrame('badge' + view);

                if (list[view]){
                    delete list[view];
                }

                if (!$rootScope.isWebClient && (_countConf() + _countPriv()) === 0){
                    $rootScope.$broadcast('sendCMDToElectron', [
                        mcConst._CMD_.ce_break_blink
                    ]);
                }
            } else
            if (count > 0){
                var itm = mcService.getFrame('badge' + view);

                if (itm){
                    itm.innerHTML = count;
                    mcService.showFrame('badge' + view);
                }

                list[view] = count;
            }

            mcService[_countConf() ? "showView" : "hideView"](_confBadge);
            mcService[_countPriv() ? "showView" : "hideView"](_privBadge);

            $rootScope.$broadcast(window._messages_.chatFrame.setUniBadge, [_countPriv() || _countPriv()]);

            if (!$rootScope.isWebClient){
                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_set_badge_count,
                    _countConf() + _countPriv()
                ]);
            }
        };

        this.checkBadges = function (clearBlink) {
            var count = _countConf() + _countPriv();

            if (!count && clearBlink && !$rootScope.isWebClient){
                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_break_blink
                ]);
            }

            // if (!$rootScope.isWebClient){
            //     $rootScope.$broadcast('sendCMDToElectron', [
            //         mcConst._CMD_.ce_set_badge_count,
            //         _countConf() + _countPriv()
            //     ]);
            // }

            return count
        };

        this.clearAll = function () {
            Object.keys(list).forEach(function (view) {
                mcService.hideFrame('badge' + view);
            });

            if (!$rootScope.isWebClient){
                $rootScope.$broadcast('sendCMDToElectron', [
                    mcConst._CMD_.ce_set_badge_count,
                    0
                ]);
            }

            list = {};
        }
    };

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

    function getPrivateSequence(){
        var list = $scope.dialogsList.serialize();
        var res  = [];

        for (var i = 0; i < list.length; i++){
            res.push(list[i].UIN);
        }

        return res;
    }

    function fillTabList() {
        tabList = []
            .concat($scope.sizes.open.conf ? $scope.channelsList.serialize() : [])
            .concat($scope.sizes.open.priv ? $scope.dialogsList.serialize()  : []);
    }

    // function updatePrivateCounter(_user){
    //     var dUIN = 'dUIN-' + _user.UIN;
    //     var item = $scope.dialogsList.getItem(dUIN);
    //     var user = {};
    //     var selected = $scope.dialogsList.getSelectedItem();
    //
    //     if (item){
    //
    //     }
    // }

    function addOrUpdatePrivate(_user){
        var dUIN = 'dUIN-' + _user.UIN;
        var item = $scope.dialogsList.getItem(dUIN);
        var user = {};
        var selected = $scope.dialogsList.getSelectedItem();

        if (!item){
            user = {
                isPrivate: true,
                id       : dUIN,
                UIN      : _user.UIN,
                Speaker  : _user.Speaker || 0,
                MsgCut   : _user.MsgCut ? _user.MsgCut.replace(/</g,"&lt;").replace(/L\$/g,"") : "",
                Name     : _user.DisplayName || "NoName",
                Badge    : _user.hasOwnProperty("Badge")     ? _user.Badge   : _user.Speaker == mcConst.UserInfo.UIN ? 0 : (_user.hasOwnProperty("IDMsgLast")   ? _user.IDMsgLast - _user.IDMsgRead : 0),
                onlineState    : _user.hasOwnProperty("onlineState")     ? _user.onlineState   : mcConst.states.offline,
                MsgType  : _user.hasOwnProperty("MsgType")   ? _user.MsgType : mcConst._CMD_.msgType.TEXT,
                dt       : _user.hasOwnProperty("dt")        ? _user.dt      : mcConst.neverDate
            };

            if (!$scope.arcDialogsList.isVisible()){
                $scope.arcDialogsList.show();
            }

            $scope.data.push(user);

            $scope.dialogsList.add(user, 0);

            // privatList[dUIN] = user;

            privateSequence = getPrivateSequence();

            fillTabList();
        } else {
            user = {
                MsgCut   : _user.hasOwnProperty("MsgCut")      ? _user.MsgCut.replace(/</g,"&lt;").replace(/L\$/g,"") : (item.MsgCut || ""),
                Name     : _user.hasOwnProperty("DisplayName") ? _user.DisplayName : (item.Name || ""),
                Speaker  : _user.hasOwnProperty("Speaker")     ? _user.Speaker     : item.Speaker,
                Badge    : _user.hasOwnProperty("Badge")       ? _user.Badge       : (_user.hasOwnProperty("IDMsgLast")   ? _user.IDMsgLast - _user.IDMsgRead : item.Badge),
                onlineState    : _user.hasOwnProperty("onlineState")       ? _user.onlineState       : item.onlineState,
                MsgType  : _user.hasOwnProperty("MsgType")     ? _user.MsgType     : item.MsgType,
                dt       : _user.hasOwnProperty("dt")          ? _user.dt          : mcConst.neverDate
            };

            if (selected && selected.UIN == _user.UIN) {
                user.Badge = 0;
            }

            item = mcService.Marge(item, user);

            $scope.dialogsList.updateItem(dUIN, item);
        }
    }

    function removePrivate(_uin){
        var dUIN = 0;
        
        if (!isNaN(parseInt(_uin))){
            dUIN = "dUIN-" + _uin;
        }

        var selected     = $scope.dialogsList.getSelectedItem();
        var removeDialog = $scope.dialogsList.getItem(dUIN);
        var position     = $scope.dialogsList.getIndexById(dUIN);

        if (removeDialog){
            // delete privatList[dUIN];

            $scope.dialogsList.remove(removeDialog.id);
            // $scope.dialogsList.refresh();

            privateSequence = getPrivateSequence();

            fillTabList();

            $rootScope.$broadcast('removeFrameChat', [$rootScope.chatAliases.UIN, _uin]);

            Badges.update(dUIN, 0);
        }

        var count = $scope.dialogsList.count();

        if ($scope.channelsList.getSelectedItem()) {
            // если есть выбранная активная конференция - выходим
            
            if (mcConst.UserInfo.hasInvite && count === 0) {
                $scope.arcDialogsList.hide();
            }
        } else
        if (count === 0){
            $scope.dialogsList.clearAll();

            if (mcConst.UserInfo.hasInvite) {
                $scope.arcDialogsList.hide();
            }

            if ($scope.channelsList.count()){
                $scope.channelsList.select($scope.channelsList.getFirstId());
            } else {
                if (mcConst.UserInfo.hasInvite){
                    $rootScope.SetPrivate(-1);

                    webix.alert(mcLang(726)); //  "726":"Все диалоги закрыты",

                    $scope.$broadcast('hide' + mcConst.dataModels.ChatWrapper);
                } else {
                    $rootScope.SetPrivate(-1);
                    $scope.$broadcast(window._messages_.dialogsList.OpenPrivate, ['0']);
                }
            }
        } else {
            if (position === count) {
                position--;
            }

            if (position !== -1){
                position = 0
            }

            setTimeout(function () {
                if (position >= 0 && selected && selected.UIN == _uin) $scope.dialogsList.select($scope.dialogsList.getIdByIndex(position));
            }, 100);
        }
    }

    function addOrUpdateConference(confInfo){
        var dUID = 'dUID-' + confInfo.UID;

        // confList[dUID] = {
        var conf = {
            id     : dUID,
            isConf : true,
            UID    : confInfo.UID,
            Badge  : confInfo.ConvID - (confInfo.IDMsgRead === -1 ? confInfo.IDMsgGot : confInfo.IDMsgRead),
            Name   : confInfo.ConfName
        };

        var hasUID = $scope.channelsList.getItem(dUID);

        if (hasUID){
            $scope.channelsList.updateItem(dUID, conf);
        } else {
            conf.userCount = 1;

            if (!$scope.arcChannelsList.isVisible()){
                $scope.arcChannelsList.show();
            }

            $scope.channelsList.add(conf);

            $scope.setHeightConf($scope.channelsList.count() * 25 + 25 + ($scope._rights.QTxtChCreate || $scope._rights.QAllowBrowseTextConfList ? 30 : 0));

            fillTabList();
        }

        if (conf.Badge) {
            $scope.channelsList.moveTop(dUID);
        }
    }

    function removeConference(UID){
        var uid = 'dUID-' + UID;

        if ($scope.channelsList.getItem(uid)){
            // delete confList[uid];

            $scope.channelsList.remove(uid);
            $rootScope.$broadcast('removeFrameChat', [$rootScope.chatAliases.UID, UID]);

            fillTabList();

            var id = $scope.channelsList.getFirstId(uid);

            if (id){
                $scope.channelsList.select(id);
            } else {
                if ($scope.dialogsList.count()) {
                    $scope.dialogsList.select($scope.dialogsList.getFirstId());

                    if ($scope.arcChannelsList.isVisible() && mcConst.UserInfo.hasInvite){
                        $scope.arcChannelsList.hide();
                    }
                } else {
                    $scope.channelsList.clearAll();

                    if (mcConst.UserInfo.hasInvite){
                        $rootScope.SetPrivate(-1);

                        webix.alert(mcLang(726)); //  "726":"Все диалоги закрыты",

                        $scope.$broadcast('hide' + mcConst.dataModels.ChatWrapper);
                    } else {
                        $rootScope.SetPrivate(-1);
                        $scope.$broadcast(window._messages_.dialogsList.OpenPrivate, ['0']);
                    }
                }

                // $rootScope.$broadcast('selectTool', [mcConst.dataModels.CommonContacts]);
            }

            $scope.setHeightConf($scope.channelsList.count() * 25 + 35 + 25);
        }
    }

    function selectDialogByType(dialog, showConf) {
        var selected = null;

        if (dialog.indexOf('UIN') >= 0 && $scope.arcDialogsList) {
            $scope.arcDialogsList.expand();

            if ($scope.dialogsList.getItem('d' + dialog)){
                selected = $scope.dialogsList.getSelectedItem();

                if (selected && selected.id === ('d' + dialog) && $rootScope.GetChatTypeID() !== dialog) {
                    $rootScope.$broadcast('OpenPrivate', [$rootScope.GetChatID()]);
                } else {
                    $scope.dialogsList.select('d' + dialog);
                }
            }
        } else
            
        if (dialog.indexOf('UID') >= 0 && $scope.arcChannelsList) {
            $scope.arcChannelsList.expand();

            if ($scope.channelsList.getItem('d' + dialog)) {
                selected = $scope.channelsList.getSelectedItem();

                if (selected && selected.id === ('d' + dialog) && showConf) {
                    $rootScope.$broadcast('OpenConf', [$rootScope.GetChatID()]);
                } else {
                    $scope.channelsList.select('d' + dialog);
                }
            }
        } else

        if ($scope.arcToolsList && mcService.findItemInArrayOfObj($scope.__toolsList, dialog.split("-")[0] + toolSuffix, "id") >= 0) {
            $scope.arcToolsList.expand();

            $scope.toolsList.select(dialog.split("-")[0] + toolSuffix);
        }
    }

    function selectNextTab(id) {
        if (tabList.length){
            $scope.channelsList.unselectAll();
            $scope.dialogsList. unselectAll();
            $scope.toolsList.   unselectAll();

            if (tabList[id].isConf) {
                $scope.channelsList.select(tabList[id].id);
            } else

            if (tabList[id].isPrivate){
                $scope.dialogsList.select(tabList[id].id);
            }

            $scope.clickOnDialogItem(tabList[id].id);
        }
    }

    function nextDialog(revert) {
        if (!tabTimer){
            var tab = revert ? (tabList.length ? tabList.length - 1 : 0) : 0;

            if (tabSelect) {
                switch ($rootScope.GetChatType()) {
                    case $rootScope.chatAliases.UIN:
                    case $rootScope.chatAliases.UID:
                        tab = mcService.findItemInArrayOfObj(tabList, tabSelect, "id");

                        if (revert) {
                            tab = tab > 0 ? tab - 1 : tabList.length - 1;
                        } else {
                            tab = tab < tabList.length - 1 ? tab + 1 : 0;
                        }
                    break;

                    case $rootScope.chatAliases.BBS:
                    case $rootScope.chatAliases.Kanban:
                        tab = revert ? (tabList.length ? tabList.length - 1 : 0) : 0;
                    break;
                }
            }

            selectNextTab(tab);

            tabTimer = setTimeout(function () {
                tabTimer = null;
            }, 70);
        }
    }

    function updateChUserCount(uid, count) {
        var dUID = 'dUID-' + uid;
        var item = $scope.channelsList.getItem(dUID);
        // var selected = $scope.channelsList.getSelectedItem();

        // if (confList.hasOwnProperty(dUID)){
        if (item){
            item.userCount = count;
            // confList[dUID].userCount = count;

            $scope.channelsList.updateItem(dUID, item);
        }
    }

    function sortDialogsList(){
        $scope.dialogsList.sort(function (a,b) {
            return mcService.sortAB(b.Badge, a.Badge);
        });

        $scope.dialogsList.sort(function (a,b) {
            return mcService.sortAB(mcService.sortableDate(b.dt), mcService.sortableDate(a.dt));
        });
    }

    function showOpenedPrivate(){
        tabSelect && $scope.dialogsList && $scope.dialogsList.getItem(tabSelect) && $scope.dialogsList.showItem(tabSelect);
    }

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

    $scope.fillConfList = function(_uin) {
        var list = $scope.channelsList.serialize();
        var uin  = _uin.split('-')[1];

        $scope.confToInvite.define("data", list);
        $scope.confToInvite.refresh();
        $scope.confToInvite.sort("id", "asc");
        $scope.confToInvite.config.UIN = uin;
    };

    $scope.inviteToConf = function(item) {
        var uin = this.getTopParentView().config.UIN;
        var uid = item.split("-")[1];

        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_forcibly_join_to_text_channel,
            mcConst.SessionID,

            uin,
            uid
        ]);

        $scope.contextDialogList.hide();
    };

    $scope.checkDrag = function (){
        return _dragItems;
    };

    $scope.chView = function(obj){
        var badge = Badges.getBadge(obj);

        return "<div class='shortMsg' style='margin-left: -5px; margin-right: 0; position: relative'>" +
            (obj.userCount ? "<span class='userCount'>(" + obj.userCount + ")</span> " : "") + (obj.Name || obj.ConfName) +
            "</div>" + badge;
    };

    $scope.tlView = function(obj){
        var badge = Badges.getBadge(obj);

        return "<div style='margin-left: -5px; margin-right: 0; position: relative'>" + obj.Name + badge + "</div>";
    };

    $scope.cnView = function (obj){
        var badge    = Badges.getBadge(obj);
        var isWebSup = obj.Name.indexOf("(websup)") === 0;
        var rColor   = $rootScope.colorPalette[obj.UIN % $rootScope.colorPalette.length];
        var name     = obj.Name.replace("(websup)", "<span class='bolder brown'>W</span>&nbsp;");
        var isIm     = obj.Speaker == mcConst.UserInfo.UIN;
        var msg      = obj.MsgCut || "...";

        switch (obj.MsgType) {
            case mcConst._CMD_.msgType.LAN_LINK:   case mcConst._CMD_.msgType.ANY_LINK: case mcConst._CMD_.msgType.CONF_LINK:
            case mcConst._CMD_.msgType.FTP_PUBLIC: case mcConst._CMD_.msgType.FTP_PERSONAL:
                msg = "<span>" + mcLang(668) + "</span>" ; // "668":"Ссылка",
            break;

            case mcConst._CMD_.msgType.IMAGE_NEW:
            case mcConst._CMD_.msgType.IMAGE:
                msg = "<span>" + mcLang(53) + "</span>" ; // "53" :"Картинка",
            break;

            case mcConst._CMD_.msgType.KANBAN_NOTIFY:
                msg = "<span>Kanban message</span>" ;
            break;

            case mcConst._CMD_.msgType.FORUM_NOTIFY:
                msg = "<span>" + mcLang(678) + "</span>" ; // "678":"Сообщение на форуме",
            break;

            case mcConst._CMD_.msgType.FILE_NEW:
            case mcConst._CMD_.msgType.FILE:
                msg = "<span>" + mcLang(568) + "</span>" ; // "568":"Файл",
            break;

            case mcConst._CMD_.msgType.MEDIA_CALL:
                msg = "<span>" + mcLang(675) + "</span>" ; // "675":"Звонок",
            break;

            case mcConst._CMD_.msgType.BARCODE:
                msg = "<span>Barcode / QRCode</span>" ;
            break;

            case mcConst._CMD_.msgType.ADM_DELETE_MESSAGE:
                msg = "<span>Message deleted</span>" ;
            break;

            case mcConst._CMD_.msgType.REPLY:
                msg = "<span>Reply</span>" ;
            break;

            case mcConst._CMD_.msgType.PRIVATE_REDIRECT:
                msg = "<span>Redirect</span>" ;
            break;

            case mcConst._CMD_.msgType.BEEP:
                msg = "<span>BEEP</span>" ;
            break;

            case mcConst._CMD_.msgType.MYCHAT_GUEST:
                msg = "<span>MyChat Guest</span>" ;
            break;

            case mcConst._CMD_.msgType.TEXT_MODIFY:
                msg = "<span>" + mcLang(762) + "</span>" ; // "762":"Сообщение изменено",
            break;

            case mcConst._CMD_.msgType.TEXT:
                if (obj.MsgCut === "{\"type\":1}"){
                    obj.MsgCut = "...";
                }
            break;
        }

        // console.log(obj.UIN + ": " + rColor + ": " + (obj.UIN % $rootScope.colorPalette.length));

        return mcService.myReplaceFormated(
            "<div style='position: relative; margin-top: 5px;' title='#{userTitle}' class='__dialog__item__' webSup='#{webSup}' uin='#{uin}'>" +
                "<span class='circleAvatar shadow2px mediumChatAvatar#{uin}' style='background-color: ##{rColor}; opacity: #{isOffline}'>#{icon}</span>" +
                "<span class='stateCircle #{hidden}'><span class='stateCircleInner boxShadow #{status}' style='background-color: #{state}'></span></span>" +
                "<div style='margin-left: 27px;'>" +
                    "#{badge}"+
                    "<div class='dialogItem nowrap shadow1px' uin='#{uin}' style='margin-top: 2px;'>#{name}</div>" +
                    "<div class='font90 shortMsg'>#{you}<span class='grayLight'>#{msg}</span></div>" + // #{color}
                "</div>" +
            "</div>",
            {
                uin   : obj.UIN,
                hidden: obj.onlineState === mcConst.states.offline ? "hidden" : "",
                state : mcService.getColorForStatus(obj.onlineState),
                status: mcConst.storageOpts.STATUSICO + obj.UIN,
                rColor: rColor,
                icon  : isWebSup ? mcService.getTwoLettersName(obj.Name.replace("(websup)", "")) : mcService.getTwoLettersName(name),
                // color : isIm ? "greenLight" : "brownLight",
                name  : name,
                badge : badge,
                you   : isIm ? mcLang(670) : "", // "670":"вы: ",
                msg   : msg,
                webSup: isWebSup,
                isOffline: obj.onlineState === mcConst.states.offline ? 0.3 : 1,
                userTitle: obj.Name.replace("(websup) ", "") + ", UIN: " + obj.UIN + (obj.onlineState === mcConst.states.offline ? ", " + mcLang(29) : "")// "29" :"Не в сети",
            }
        );
    };

    $scope.registerHotKeys = function () {
        $rootScope.hotKeyDispatcher.addPreset($scope.Name, [{
            key   : mcConst.keyCodes.tab,
            ctrlKey: true,
            func  : function () { nextDialog(); }
        }, {
            key   : mcConst.keyCodes.pagedown,
            ctrlKey: true,
            func  : function () { nextDialog(); }
        }, {
            key   : mcConst.keyCodes.pageup,
            ctrlKey: true,
            func  : function () { nextDialog(true); }
        }, {
            key   : mcConst.keyCodes.tab,
            shiftKey: true,
            func  : function () { nextDialog(true); }
        }], document);
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset($scope.Name);
    };

    $scope.saveOptions = function () {
        if (!saveTimerOption) {
            saveTimerOption = setTimeout(function () {
                $rootScope.Storage.dialogs.save(null, null, mcConst.storageOpts.DIALOGOPT, JSON.stringify({
                    sizes: $scope.sizes
                }), function () {
                    saveTimerOption = null;
                });

                fillTabList();
            }, 300);
        }
    };

    $scope.setHeightConf = function (height) {
        $scope.arcChannelsList.define("height", height);
        $scope.arcChannelsList.resize();

        $scope._accor2.resizeChildren();
    };

    $scope.setHeightTool = function (height) {
        $scope._accor1.define("height", height);
        $scope._accor1.resize();

        $scope._accor1.resizeChildren();
    };

    $scope.beforeSelect = function(){
        prevID = $scope.dialogsList.getSelectedId();
    };

    $scope.openChannelsManager = function () {
        $rootScope.$broadcast('show' + mcConst.dataModels.ConfManager, []);
    };

    $scope.openCreateConf = function () {
        $scope.channelsList.unselectAll();
        $scope.dialogsList .unselectAll();
        $scope.toolsList   .unselectAll();

        _dragItems = true;

        // $scope.dialogsList.define("drag", "source");
        // $scope.dialogsList.refresh();

        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.CreateConf]);
    };

    $scope.selectTools = function(id){
        $scope.dialogsList.unselectAll();
        $scope.channelsList.unselectAll();

        switch (id){
            case mcConst.dataModels.Admin + toolSuffix:
                $rootScope.SetAdmin();
            break;

            case mcConst.dataModels.Forum + toolSuffix:
                $rootScope.SetForum();
            break;

            case mcConst.dataModels.BBS + toolSuffix:
                $rootScope.SetBBS();
            break;

            case mcConst.dataModels.Kanban + toolSuffix:
                $rootScope.SetKanban();
                $rootScope.$broadcast(window._messages_.kanban.openKanbanUrl, [mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasKanban + "/"]);
            break;

            case mcConst.dataModels.SendBroadcast + toolSuffix:
                $rootScope.SetSendBroadcast();
            break;
        }

        tabSelect = null;

        $rootScope.$broadcast('selectTool');
        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [id.replace(toolSuffix, '')]);
    };

    $scope.clickOnDialogItem = function (id) {
        var item   = $scope.dialogsList.getItem(id);

        if (item) {
            // if (item.id) {
            //     $rootScope.$broadcast('selectTool',   [item.id.indexOf("UID") >= 0 ? mcConst.dataModels.ConfUserList : (item.id.indexOf("UIN") >= 0 ? mcConst.dataModels.PrivateInfo : null)]);
            // }

            $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.ChatFrame, id]);
        }
    };

    $scope.openPrivateDialog = function(dUIN){
        var item   = $scope.dialogsList.getItem(dUIN);

        if (item){
            // var _badge = privatList[dUIN] ;

            if (item.Badge !== 0) {
                item.Badge = 0;

                Badges.update(dUIN, item.Badge);
            }

            $scope.channelsList.unselectAll();
            $scope.toolsList.unselectAll();

            tabSelect = dUIN;

            $rootScope.$broadcast('OpenPrivate', [item.UIN]);

            showOpenedPrivate();
        }
    };

    $scope.openChannelDialog = function(id){
        var item = $scope.channelsList.getItem(id);

        // var _badge = confList[id];

        if (item && item.Badge !== 0) {
            item.Badge = 0;

            Badges.update(id, item.Badge);
        }

        tabSelect = id;

        $scope.dialogsList.unselectAll();
        $scope.toolsList.unselectAll();

        $rootScope.$broadcast('OpenConf', [item.UID]);
    };

    $scope.updateCounter = function(user, dontInc){
        // "IDMsgLast"   : 4567,                  // ID последнего отправленного сообщения
        // "IDMsgGot"    : 4560,                  // ID последнего полученного сообщения
        // "IDMsgRead"   : 4560,                  // ID последнего прочитанного сообщения
        var UID  = user.UID;
        var UIN  = user.UINFrom || user.UIN ? (user.UINFrom || user.UIN).toString() : 0;
        var nick = user.Nick || user.DisplayName || "NoName";

        if (UIN == mcConst.UserInfo.UIN){
            UIN = user.UINTo.toString();
            
            dontInc = true;
        }

        var itemID  = UID !== void(0) ? 'dUID-' + UID : 'dUIN-' + UIN;
        var list    = UID !== void(0) ? $scope.channelsList : $scope.dialogsList;
        var item    = list.getItem(itemID);
        var newBadge = user.hasOwnProperty("IDMsgLast") ? user.IDMsgLast - user.IDMsgRead : item.Badge;

        if (UIN && !UID && (
            item.MsgType !== user.MsgType ||
            item.MsgCut  !== user.MsgCut ||
            item.Name    !== nick ||
            item.onlineState   !== user.onlineState ||
            item.Badge   !== newBadge ||
            user.MsgType === mcConst._CMD_.msgType.BEEP
        )){

            addOrUpdatePrivate({
                DisplayName: nick,
                isPrivate  : true,
                UIN        : UIN,
                onlineState: user.onlineState,
                MsgCut     : user.MsgCut || "",
                Badge      : newBadge,
                MsgType    : user.hasOwnProperty("MsgType") ? user.MsgType : mcConst._CMD_.msgType.TEXT,
                Speaker    : user.hasOwnProperty("Speaker") ? user.Speaker : mcConst.states.offline,
                dt         : user.hasOwnProperty("dt")      ? user.dt      : mcService.formatDate(new Date(), "dd.mm.yyyy.hh.nn.ss")
            });
        }

        if (list.getItem(itemID)){
            if (item.Badge || (!item.Badge && !dontInc)) {
                list.moveTop(itemID);
            }

            if (!UID) {
                privateSequence = getPrivateSequence();
            } else {
                item.Badge = newBadge;
            }

            if (list.getSelectedId() !== itemID && item){
                if (!dontInc) item.Badge ++;

                Badges.update(itemID, item.Badge);

                fillTabList();
            }
        }
    };

    $scope.removePrivateDialog = function (id, type) {
        var uin = id.split('-')[1];

        // console.log('Remove dialog, UIN: ' + uin);

        if (uin !== void(0) && id && (mcConst.ClientSettings.GeneralDoubleClickPagesClose || type === "rightClick")) {
            $rootScope.$broadcast('removeDialogFromList', [uin]);
        }
    };

    $scope.show = function(){
        mcService.updateStatuses(userStatuses, $scope.data);

        contacts = initDialogs($scope);

        $scope.registerHotKeys();

        showOpenedPrivate();
    };

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

    var _msg = window._messages_.dialogsCtrl = {
        allUsersOnlineStatusesChanged   : 'allUsersOnlineStatusesChanged',
        getUserSequence                 : 'getUserSequence',
        isConfOpen                      : 'isConfOpen',
        addOrUpdateConfDialogs          : 'addOrUpdateConfDialogs',
        removePrivateFromDialogs        : 'removePrivateFromDialogs',
        removeConfFromDialogs           : 'removeConfFromDialogs',
        addOrUpdatePrivateDialog        : 'addOrUpdatePrivateDialog',
        selectDialog                    : 'selectDialog',
        updateCounter                   : 'updateCounter',
        receiveBBS                      : 'receiveBBS',
        updateChUserCount               : 'updateChUserCount',
        on_mainWindowFocused            : 'on_mainWindowFocused',
        clearAllDialogsMessages         : 'clearAllDialogsMessages',
        clearAllConfMessages            : 'clearAllConfMessages',
        updateDialogInfo                : 'updateDialogInfo',
        sortDialogsList                 : 'sortDialogsList',
        changeStateForUser              : 'changeStateForUser',
        disableContactsDD               : 'disableContactsDD',
        getChannelsList                 : 'getChannelsList',
        getFirstDialog                  : 'getFirstDialog',
        // updatePrivateCounter            : 'updatePrivateCounter'
    };

    $scope.$on(_msg.getFirstDialog, function(){
        return privateSequence.length ? privateSequence[0] : 0;
    });

    $scope.$on(_msg.getChannelsList, function(){
        return $scope.channelsList ? $scope.channelsList.serialize() : [];
    });

    $scope.$on(_msg.disableContactsDD, function(){
        _dragItems = false;
    });

    $scope.$on(_msg.changeStateForUser, function(e, args){
        addOrUpdatePrivate({
            UIN  : args[0],
            onlineState: args[1]
        });
    });

    $scope.$on(_msg.allUsersOnlineStatusesChanged, function(e, args){
        userStatuses = args[0];

        mcService.updateStatuses(userStatuses, $scope.data);

        if ($scope.dialogsList){
            $scope.dialogsList.refresh();
        }
    });

    $scope.$on(_msg.on_mainWindowFocused, function(){
        if (mcConst.LoggedIn){
            Badges.checkBadges(true);

            // selectDialogByType($rootScope.GetChatTypeID(), true);
        }
    });

    $scope.$on(_msg.getUserSequence, function(e, args){
        var callback = args[0];

        if (callback) {
            callback(privateSequence);
        }
    });

    $scope.$on(_msg.isConfOpen, function(e, args){
        var uid = args[0];
        var callback = args[1];

        // if (callback) callback(confList.hasOwnProperty(uid));
        if (callback) callback(!!$scope.channelsList.getItem(uid));
    });

    $scope.$on(_msg.addOrUpdateConfDialogs, function(e, args){
        addOrUpdateConference.apply(null, args);
    });

    $scope.$on(_msg.clearAllDialogsMessages, function(){
        privateSequence = [];
        prevID          = 0;
        userStatuses    = null;
        tabList         = [];

        if ($scope.dialogsList) {
            $scope.dialogsList.serialize().forEach(function (priv){
                $rootScope.$broadcast(window._messages_.dialogsList.clearPrivHistory, [priv.UIN, 0]);

                $rootScope.$broadcast(window._messages_.dialogsList.removeDialogFromList, [priv.UIN]);
            });

            // $scope.dialogsList.clearAll();

            $rootScope.$broadcast(window._messages_.dialogsList.clearPrivatesOnReconnect);
        }

        // $rootScope.$broadcast('OpenPrivate', [0]);
    });

    $scope.$on(_msg.clearAllConfMessages, function(){
        $scope.channelsList && $scope.channelsList.serialize().forEach(function (conf){
            $rootScope.$broadcast(window._messages_.dialogsList.clearConfHistory, [conf.UID, 0]);
        });
    });

    $scope.$on(_msg.removePrivateFromDialogs, function(e, args){
        removePrivate.apply(null, args);
    });

    $scope.$on(_msg.removeConfFromDialogs, function(e, args){
        removeConference.apply(null, args);
    });

    $scope.$on(_msg.receiveBBS, function(){
        if (!$rootScope.isWebClient && mcConst.ClientSettings.EventsPopupOnNewBBS){
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_show_on_top
            ]);
        }

        mcPlaySound.Play(mcPlaySound.Sounds.BBS);

        setTimeout(function () {
            $scope.arcToolsList.expand();
            $scope.toolsList.select(mcConst.dataModels.BBS + toolSuffix);
            
            $rootScope.$broadcast(window._messages_.BBS.refreshBBS, [mcConst.ClientSettings.EventsTrayBBSMsg]);
        }, 10);
    });

    $scope.$on(_msg.selectDialog, function(e, args){
        selectDialogByType.apply(null, args);
    });

    $scope.$on(_msg.addOrUpdatePrivateDialog, function(e, args){
        addOrUpdatePrivate.apply(null, args);

        Badges.checkBadges(true);
    });

    // $scope.$on(_msg.updatePrivateCounter, function(e, args){
    //     updatePrivateCounter.apply(null, args);
    // });

    $scope.$on(_msg.updateDialogInfo, function(e, args){
        var UIN  = 'dUIN-' + args[0];
        var info = args[1];
        var user = $scope.dialogsList.getItem(UIN);

        if (user){
            user = mcService.Marge(user, info);

            $scope.dialogsList.updateItem(UIN, user);
        }
    });

    $scope.$on(_msg.updateCounter, function(e, args){
        $scope.updateCounter.apply(null, args);

        Badges.checkBadges(true);
    });

    $scope.$on(_msg.updateChUserCount, function(e, args){
        updateChUserCount.apply(null, args);
    });

    $scope.$on(_msg.sortDialogsList, function(e, args){
        sortDialogsList.apply(null, args);
    });

    $scope.$on('hide' + $scope.Name, function(){
        contacts.hide();

        $scope.removeHotKeys();
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        $scope.wndSize = $rootScope.wndSize;

        $scope.show();

        $rootScope.Storage.dialogs.load(null, null, mcConst.storageOpts.DIALOGOPT, function (data) {
            var info = mcService.StringToObj(data);

            if (mcService.isObject(info)) {
                $scope.sizes = info.sizes;

                // $scope.setHeightConf($scope.sizes.conf);
                // $scope.setHeightTool($scope.sizes.tool);

                $scope.arcDialogsList [$scope.sizes.open.priv ? "expand" : "collapse"]();
                $scope.arcToolsList   [$scope.sizes.open.tool ? "expand" : "collapse"]();
                $scope.arcChannelsList[$scope.sizes.open.conf ? "expand" : "collapse"]();
            }
        });
    });

    $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
        $scope._rights.QMessengerStyle = mcService.checkRights(mcConst._CMD_.RS.QMessengerStyle);
        $scope._rights.QKanbanAccess   = mcService.checkRights(mcConst._CMD_.RS.QKanbanAccess);
        $scope._rights.QWEBAllowAdmin  = mcService.checkRights(mcConst._CMD_.RS.QWEBAllowAdmin);
        $scope._rights.QForumAccess    = mcService.checkRights(mcConst._CMD_.RS.QForumAccess);
        $scope._rights.QBBSView        = mcService.checkRights(mcConst._CMD_.RS.QBBSView);
        $scope._rights.QBroadcasts     = mcService.checkRights(mcConst._CMD_.RS.QBroadcasts);
        $scope._rights.QTxtChCreate    = mcService.checkRights(mcConst._CMD_.RS.QTxtChCreate);
        $scope._rights.QTxtChJoin      = mcService.checkRights(mcConst._CMD_.RS.QTxtChJoin);
        $scope._rights.QAllowInvites   = mcService.checkRights(mcConst._CMD_.RS.QAllowInvites);
        $scope._rights.QAllowBrowseTextConfList = mcService.checkRights(mcConst._CMD_.RS.QAllowBrowseTextConfList);
        // $scope._rights.QForciblyInvites= mcService.checkRights(mcConst._CMD_.RS.QForciblyInvites);

        $scope.__toolsList = []
           .concat($scope._rights.QWEBAllowAdmin ? [{ Name: mcService.insertIco("fa-gear toolIcon")      + "Admin",     Badge: 0, id: mcConst.dataModels.Admin  + '-tool'}] : []) //
           .concat($scope._rights.QKanbanAccess  ? [{ Name: mcService.insertIco("fa-tasks toolIcon")     + "Kanban",    Badge: 0, id: mcConst.dataModels.Kanban + '-tool'}] : []) //
           .concat($scope._rights.QForumAccess   ? [{ Name: mcService.insertIco("fa-bookmark toolIcon")  + mcLang(661), Badge: 0, id: mcConst.dataModels.Forum  + '-tool'}] : []) // "661":"Форум",
           .concat($scope._rights.QBBSView       ? [{ Name: mcService.insertIco("fa-calendar-o toolIcon")+ mcLang(556), Badge: 0, id: mcConst.dataModels.BBS    + '-tool'}] : []) // "556":"Доска объявлений",
           .concat($scope._rights.QBroadcasts    ? [{ Name: mcService.insertIco("fa-bullhorn toolIcon")  + mcLang(632), Badge: 0, id: mcConst.dataModels.SendBroadcast + '-tool'}] : []) // "632":"Оповещение",
           // .concat(!mcConst.UserInfo.hasInvite   ? [{ Name: mcService.insertIco("fa-star-o toolIcon")    + mcLang(719), Badge: 0, id: mcConst.dataModels.Important + '-tool'}] : []) // "719":"Важное",
        ;

        $scope.channelBtnWrapper[$scope._rights.QTxtChCreate || $scope._rights.QAllowBrowseTextConfList ? "show" : "hide"]();
        $scope.channelManager   [$scope._rights.QAllowBrowseTextConfList ? "show" : "hide"]();
        $scope.createChannel    [$scope._rights.QTxtChCreate             ? "show" : "hide"]();
        $scope.inviteButtonMenu [$scope._rights.QAllowInvites            ? "show" : "hide"]();
        
        if (!$scope.__toolsList.length){
            $scope.arcToolsList && $scope.arcToolsList.hide();
        } else
        if ($scope.__toolsList.length && $scope.arcToolsList && !$scope.arcToolsList.isVisible()) {
            $scope.arcToolsList.show();
        }

        if ($scope.toolsList){
            $scope.toolsList.clearAll();
            $scope.toolsList.parse($scope.__toolsList);
            $scope.toolsList.define("height", $scope.__toolsList.length * 25);
            $scope.toolsList.resize();
        }
    });
}


// var tt = {
//     "ua_text": "MyChat 8.0 \u2014 \u0432\u0435\u043b\u0438\u043a\u0435 \u043e\u043d\u043e\u0432\u043b\u0435\u043d\u043d\u044f",
//     "ua_link": "https:\/\/nsoft-s.com\/ua\/news\/1343-mychat-8.html",
//     "en_text": "MyChat 8.0 \u2014 major update",
//     "en_link": "https:\/\/nsoft-s.com\/en\/news\/1342-mychat-8.html",
//     "v"      : "8.0",
//     "ru_text": "MyChat 8.0 \u2014 \u0431\u043e\u043b\u044c\u0448\u043e\u0435 \u043e\u0431\u043d\u043e\u0432\u043b\u0435\u043d\u0438\u0435",
//     "ru_link": "https:\/\/nsoft-s.com\/news\/1341-mychat-8.html",
//     "dt"     : "08.07.2020"
// }
function initMainMenu($scope){
    "use strict";

    var __view = $$($scope.Name);

    if (!__view){
        // var statuses = [
        //     { id: "0", value: "<span class='green'>" + mcLang(534) + "</span>" }, // "534":"В сети",
        //     { id: "1", value: "<span class='red'>" + mcLang(535) + "</span>" }  // "535":"Не беспокоить",
        // ];

        var menu = { id: $scope.Name, css: "darkenNavBG", rows: [
            { view: "button", label: mcLang(45), css: "imgBtn icoButton button_transparent", type: "icon", icon: "book", // "45" : "Профиль",
                on: { onItemClick:  $scope.profile}
            },

            //{ view: "segmented", options: statuses, value: mcConst.UserInfo.Status, css: "whiteBG yellowSelect", on: { onChange: $scope.changeStatus }, autowidth: true },
            //{ view: "button", label: mcLang(15), click: $scope.settings, css: "imgBtn icoButton button_transparent", type: "icon", icon: "gear"}, // "15" :"Настройки",

            { id: "logsBtn" + $scope.Name, view: "button", label: mcLang(586), css: "imgBtn icoButton button_transparent", hidden: $scope.isWebClient, type: "icon", icon: "file-text-o", // "586":"Протоколы",
                on: { onItemClick:  $scope.logs}
            },

            { id: "settingsBtn" + $scope.Name, view: "button", label: mcLang(15), css: "imgBtn icoButton button_transparent menuBTN",
                on: {onItemClick:  $scope.settings}, hidden: $scope.isWebClient, disabled: !$scope._rights.QAllowOptionsChange, type: "icon", icon: "cog"}, // "15" :"Настройки",

            { id: "terminalBtn" + $scope.Name, view: "button", label: "Terminal", css: "imgBtn icoButton button_transparent menuBTN",
                on: {onItemClick:  $scope.terminal}, hidden: $scope.isWebClient, type: "icon", icon: "terminal"},

            { id: "historyBroadcastBtn" + $scope.Name, view: "button", label: mcLang(665), icon: "calendar", css: "imgBtn icoButton button_transparent menuBTN", type: "icon", // "665":"История оповещений",
                hidden: true,
                on: { onItemClick: $scope.brHistory}
            },

            {},

            { id: "logoutBtn" + $scope.Name, view: "button", label: mcLang(536), css: "imgBtn icoButton button_transparent", type: "icon", icon: "sign-out", // "536":"Сменить пользователя",
                hidden: !$scope._rights.QAllowAccountsManager,
                on: { onItemClick:  $scope.logout}
            },

            { id: "quitBtn" + $scope.Name,   view: "button", label: mcLang(47),   css: "imgBtn icoButton button_warning button_transparent", // "47" : "Выйти",
                on: { onItemClick: $scope.quit}, hidden: $scope.isWebClient, disabled: !$scope._rights.QAllowCloseProgram, type: "icon", icon: "power-off"}
        ]};

        $$($scope.container).addView(menu);

        __view = $$($scope.Name);

        $scope.settingsBtn  = $$("settingsBtn" + $scope.Name);
        $scope.terminalBtn  = $$("terminalBtn" + $scope.Name);
        $scope.quitBtn      = $$("quitBtn" + $scope.Name);
        $scope.logoutBtn    = $$("logoutBtn" + $scope.Name);
        $scope.logsBtn      = $$("logsBtn" + $scope.Name);
        $scope.historyBroadcastBtn  = $$("historyBroadcastBtn" + $scope.Name);

        mcService.disableContextMenu(__view);
    } else {
        __view.show();
    }

    return __view;
}
function mainMenuController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.MainMenu;

    var variants = {
        PROFILE     : 1,
        SETTINGS    : 2,
        LOGS        : 3,
        BR_HISTORY  : 4,
        TERMINAL    : 5
    };

    var view = null;
    var MAIN_MENU = 'MAIN_MENU';
    var currentMenu = 0;
    var restoreTool = null;

    $scope._rights = {
        QAllowOptionsChange: false,
        QAllowCloseProgram: false,
        QAllowAccountsManager: false,
        QWEBAllowViewSystemLog: false
    };

    function selectMenu(name){
        switch (name) {
            case "brcHistory":
                $scope.historyBroadcastBtn.callEvent("onItemClick");
            break;

            case mcConst.dataModels.Terminal:
                $scope.terminalBtn.callEvent("onItemClick");
            break;
        }
    }

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

    $scope.profile = function(){
        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.UserProfile, mcConst.UserInfo.UIN]);

        currentMenu = variants.PROFILE;
    };

    $scope.logs = function () {
        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.ViewLogs, mcConst.UserInfo.UIN]);

        currentMenu = variants.LOGS;
    };

    $scope.settings = function () {
        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.Settings]);

        currentMenu = variants.SETTINGS;
    };

    $scope.terminal = function () {
        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.Terminal]);

        currentMenu = variants.TERMINAL;
    };

    $scope.brHistory = function(){
        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.HistoryBroadcast]);

        currentMenu = variants.BR_HISTORY;
    };

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

    $scope.registerHotKeys = function () {
        $rootScope.hotKeyDispatcher.addPreset(MAIN_MENU, [{
            key     : mcConst.keyCodes.esc,
            lockPrev: true,
            func    : function () {
                $scope.removeHotKeys();

                $rootScope.$broadcast(window._messages_.chatWrapper.showHideMenu);
            }
        }], document);
    };

    $scope.changeStatus = function (newStatus) {

    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset(MAIN_MENU);
    };

    $scope.logout = function(){
        if ($scope._rights.QAllowAccountsManager){
            mcService.ClearSessionSettings(true, true);

            if (!$rootScope.isWebClient){
                $rootScope.UNIT.ctrlServersManager.setAutoConnectInfo("", false, "");
            }

            $rootScope.$broadcast('SendCMDToServer' , [
                mcConst.lockInterface,
                mcConst._CMD_.cs_quit,
                mcConst.SessionID
            ]);

            setTimeout(function(){
                $rootScope.$broadcast('StopPingTimer');

                location.search = "";
                location.reload();
            }, 200);
        }
    };

    $scope.quit = function(){
        $scope._rights.QAllowCloseProgram && $rootScope.$broadcast("quitFromProgram");
    };

    $scope.show = function(){
        $scope.isWebClient =  $rootScope.isWebClient;

        view = initMainMenu($scope);

        $scope.registerHotKeys();
    };

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

    var _msg = window._messages_.mainMenu = {
        logoutMenu          : 'logoutMenu',
        selectMenuElement   : 'selectMenuElement',
    };

    $scope.$on(_msg.selectMenuElement, function (e, args) {
        selectMenu.apply(null, args);
    });

    $scope.$on(_msg.logoutMenu, $scope.logout);

    $scope.$on('hide' + $scope.Name, function(){
        if (view.isVisible()){
            $scope.removeHotKeys();

            view.hide();

            switch ($rootScope.GetChatType()) {
                case $rootScope.chatAliases.UID:
                case $rootScope.chatAliases.UIN:
                    $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.ChatFrame, 'd' + $rootScope.GetChatTypeID()]);
                break;

                default:
                    $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [restoreTool]);
            }
        }

        $rootScope.$broadcast(window._messages_.chatFrame.focusToEnterChat);

        restoreTool = null;
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        restoreTool = $rootScope.GetChatType();

        if (mcService.checkNoRestoreTool(restoreTool)){
            restoreTool = null;
        }

        $scope.wndSize = $rootScope.wndSize;

        $scope.show();

        $rootScope.$broadcast(window._messages_.chatFrame.focusToEnterChat);
    });

    $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
        $scope._rights.QAllowOptionsChange      = mcService.checkRights(mcConst._CMD_.RS.QAllowOptionsChange);
        $scope._rights.QAllowCloseProgram       = mcService.checkRights(mcConst._CMD_.RS.QAllowCloseProgram);
        $scope._rights.QAllowAccountsManager    = mcService.checkRights(mcConst._CMD_.RS.QAllowAccountsManager);
        $scope._rights.QWEBAllowViewSystemLog   = mcService.checkRights(mcConst._CMD_.RS.QWEBAllowViewSystemLog);

        $scope.settingsBtn && $scope.settingsBtn[$scope._rights.QAllowOptionsChange     ? 'enable': 'disable']();
        $scope.quitBtn     && $scope.quitBtn    [$scope._rights.QAllowCloseProgram      ? 'enable': 'disable']();
        $scope.logoutBtn   && $scope.logoutBtn  [$scope._rights.QAllowAccountsManager   ? 'enable': 'disable']();
        $scope.logsBtn     && $scope.logsBtn    [$scope._rights.QWEBAllowViewSystemLog  ? 'enable': 'disable']();
    });
}

function initSearch($scope){
    "use strict";

    var __view = $$($scope.Name);

    if (!__view){
        var menu = { id: $scope.Name, css: "", rows: [
            { height: 10},
                
            {   id        : "findResultList" + $scope.Name,
                view      : "list",
                css       : "noBGColor listColorWhite lineHeight20 scrollMenu dialogs_list hiddenScrollMenu thinScrollBar",
                borderless: true,
                select    : true,
                template  : $scope.cnView,
                navigation: true,
                scroll    : "y",
                type      : {
                    headerHeight:30,
                    height: 44
                },
                on: {
                    onItemClick    : $scope.clickUser,
                    onItemDblClick : $scope.openUser,
                    onAfterSelect  : function (id) { this.showItem(id); }
                }
            },

            { id: "noUsersFind" + $scope.Name, template: mcLang(660), borderless: true, hidden: true, css: "white byCenter"} // "660":"Ничего не найдено",
        ]};

        $$($scope.container).addView(menu);

        __view = $$($scope.Name);

        $scope.findResultList   = $$("findResultList" + $scope.Name);
        $scope.noUsersFind      = $$("noUsersFind" + $scope.Name);

        mcService.disableContextMenu(__view);
    } else {
        __view.show();
        
        $scope.noUsersFind.hide();
    }

    return __view;
}
function searchController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.Search;

    var view         = null;
    var currentRes   = 0;
    var currentID    = 0;
    var currentQuery = "";
    var SEARCH_USERS = "SEARCH_USERS";
    var restoreTool  = null;

    function findUsers(query){
        if (query && currentQuery !== query){
            show();

            currentQuery = query;

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_find_users,
                mcConst.SessionID,

                query,
                "DISPLAYNAME,STATE,UIN,SEX,EMAIL",
                30,

                function (res) {
                    currentRes = res;

                    delete currentRes.Fields;

                    if (currentRes[mcConst.UserInfo.UIN]) {
                        delete currentRes[mcConst.UserInfo.UIN];
                    }

                    currentRes = mcService.convertObjToArray(currentRes, "UIN");

                    if (currentRes.length){
                        $scope.findResultList.clearAll();
                        $scope.findResultList.show();
                        $scope.findResultList.parse(currentRes);

                        currentID = $scope.findResultList.getFirstId();

                        $scope.findResultList.select(currentID);
                    } else {
                        $scope.findResultList.hide();
                        $scope.noUsersFind.show();
                    }
                }
            ]);
        }
    }

    function show(container){
        restoreTool = $rootScope.GetChatType();

        if (mcService.checkNoRestoreTool(restoreTool)){
            restoreTool = null;
        }

        $scope.container   = container || mcConst.containers.left;
        $scope.wndSize     = $rootScope.wndSize;
        $scope.isWebClient = $rootScope.isWebClient;

        view = initSearch($scope);

        $scope.registerHotKeys();
    }

    function hide(){
        if (view && view.isVisible()){
            $scope.removeHotKeys();

            view.hide();

            switch ($rootScope.GetChatType()) {
                case $rootScope.chatAliases.UID:
                case $rootScope.chatAliases.UIN:
                    $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.ChatFrame, 'd' + $rootScope.GetChatTypeID()]);
                break;

                default:
                    $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [restoreTool]);
            }

            $rootScope.$broadcast('show' + mcConst.dataModels.Dialogs, [mcConst.containers.left]);
        }

        currentQuery = "";
        currentRes   = [];
        currentID    = 0;
        restoreTool  = null;
    }

    function nextUser(){
        if (currentRes.length){
            if (currentID === $scope.findResultList.getLastId()) {
                currentID = $scope.findResultList.getFirstId();
            } else {
                currentID = $scope.findResultList.getNextId(currentID);
            }

            $scope.findResultList.select(currentID);
        }
    }

    function prevUser(){
        if (currentRes.length){
            if (currentID === $scope.findResultList.getFirstId()) {
                currentID = $scope.findResultList.getLastId();
            } else {
                currentID = $scope.findResultList.getPrevId(currentID);
            }

            $scope.findResultList.select(currentID);
        }
    }

    function openCurrent(){
        if (currentRes.length){
            var user = $scope.findResultList.getSelectedItem();

            if (user) {
                $rootScope.$broadcast('OpenPrivate', [user.UIN]);
            }
        }
    }

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

    $scope.cnView = function(obj){
        var isWebSup = obj.DISPLAYNAME.indexOf("(websup)") === 0;
        var rColor   = $rootScope.colorPalette[obj.UIN % $rootScope.colorPalette.length];
        var name     = obj.DISPLAYNAME.replace("(websup)", "<span class='bolder brown'>W</span>&nbsp;");

        // var privateIcon = mcService.myReplaceFormated(
        //     "<span class='webix_icon fa-user smaller #{icon}' style='#{color}'></span>",
        //     {
        //         icon : mcConst.storageOpts.STATUSICO + obj.UIN,
        //         color: "color:" + mcService.getColorForStatus(obj.STATE)
        //     }
        // );

        return mcService.myReplaceFormated(
            // "<span class='dialogItem nowrap' uin='#{uin}'>#{icon} #{name}</span>",
            // {
            //     icon: privateIcon,
            //     uin : obj.UIN,
            //     name: obj.DISPLAYNAME.replace("(websup)", "<span class='bolder brown'>W</span>&nbsp;")
            // }
            "<div style='position: relative'>" +
                "<span class='circleAvatar shadow2px boxShadow' style='background-color: ##{rColor}; opacity: #{isOffline}'>#{icon}</span>" +
                "<span class='stateCircle'><span class='stateCircleInner boxShadow' style='background-color: #{state}'></span></span>" +
                "<div style='margin-left: 27px;'>" +
                    "<span class='dialogItem nowrap shadow1px' uin='#{uin}' style='margin-top: 2px;'>#{name}</span>" +
                    "<span class='font85 shortMsg grayLight'>UIN: #{uin}, #{email}</span>" +
                "</div>" +
            "</div>",
            {
                email : obj.EMAIL,
                uin   : obj.UIN,
                state : mcService.getColorForStatus(obj.STATE),
                rColor: rColor,
                icon  : isWebSup ? mcService.getTwoLettersName(obj.DISPLAYNAME.replace("(websup)", "")) : mcService.getTwoLettersName(name),
                name  : name,
                isOffline: obj.STATE == mcConst.states.offline ? 0.35 : 1
            }
        );
    };

    $scope.clickUser = function(){
        currentID = $scope.findResultList.getSelectedId();
    };

    $scope.openUser = function(){
        $rootScope.$broadcast('OpenPrivate', [$scope.findResultList.getSelectedItem().UIN]);
    };

    $scope.registerHotKeys = function () {
        $rootScope.hotKeyDispatcher.addPreset(SEARCH_USERS, [{
            key     : mcConst.keyCodes.esc,
            lockPrev: true,
            func    : function () {
                // hide();
                $rootScope.$broadcast(window._messages_.chatWrapper.closeGlobalSearch);
            }
        }], document);
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset(SEARCH_USERS);
    };

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

    var _msg = window._messages_.search = {
        searchUsers     : "searchUsers",
        openCurrent     : "openCurrent",
        prevUser        : "prevUser",
        nextUser        : "nextUser"
    };

    $scope.$on(_msg.openCurrent, function (e, args) {
        openCurrent.apply(null, args);
    });

    $scope.$on(_msg.nextUser, function (e, args) {
        nextUser.apply(null, args);
    });

    $scope.$on(_msg.prevUser, function (e, args) {
        prevUser.apply(null, args);
    });

    $scope.$on(_msg.searchUsers, function (e, args) {
        findUsers.apply(null, args);
    });

    $scope.$on('hide' + $scope.Name, function(){
        hide();
     });

    $scope.$on('show' + $scope.Name, function(e, args){
        show.apply(null, args);
    });
}

function initConfManagerView($scope){
    "use strict";

    var view = $$($scope.Name);

    if (!view){
        var filter = new mcService.FilterFiled({
            listName     : 'existingConfs' + $scope.Name,
            placeholder  : mcLang(35), // "35" : "Название конференции",
            css          : "noBorder",
            noClearBtn   : true,
            filterParams : ['Name', 'Topic'],
            id           : "conManagerFilterField",
            OnEnterText  : function(text){
                text === '' ? $scope.createNewConfBtn.disable() : $scope.createNewConfBtn.enable();

                $scope.confName = text;
            }
        });

        view = webix.ui({
            view    : "window",
            modal   : true,
            position: 'center',
            id      : $scope.Name,
            width   : isMobile ? $scope.wndSize.width : 400,
            height  : $scope.wndSize.height,
            head    : { cols:[
                { width: 42 },
                { view : "label",  label:'<div class="byCenter toUpper">' + mcService.Lang(686) + "</div>"}, // "686":"Список конференций",
                { view : "button", type: 'icon', width: 42, icon: 'times', css: "button_transparent", click: $scope.hideWnd }
            ]},
            body    : { rows: [
                filter,

                { view : 'list', id: 'existingConfs' + $scope.Name, maxHeight: 300, scroll: true, data: [], css: "hiddenScrollMenu thinScrollBar",
                    type: {
                        height: "auto"
                    },
                    template  : function(obj) {
                        return mcService.myReplaceFormated(
                            "<div class='lHeight23'>#{icon}<span class='gray font90'>(#{count})</span> <b>#{name}</b></div>" +
                            "<div class='lHeight20 gray' style='display: #{hasTopic}; padding-left: 22px'>#{topic}</div>",
                            {
                                icon    : mcService.insertIco(obj.ConfType === mcConst._CMD_.confType.PASSWORD ? "fa-key" : obj.ConfType === mcConst._CMD_.confType.STANDARD ? "fa-users" : "fa-eye", 12),
                                name    : obj.Name.replace(/\</g, "&lt;"),
                                count   : obj.UsersCount,
                                topic   : (obj.Topic || "").replace(/\</g, "&lt;"),
                                hasTopic: obj.Topic ? "block" : "none"
                            }
                        );
                    },
                    on: {
                        onItemClick: function(id){
                            $scope.selectedConf = id;

                            $scope.openConf(id, null, $scope.existingConfs.getItem(id).ConfType === mcConst._CMD_.confType.PASSWORD);
                        }
                    }
                },

                { view: "button", id: 'createNewConfBtn', value: mcLang(5), disabled: true, click: $scope.openOrCreate } // "5" : "Войти",
            ]},
            on: {
                onShow    : $scope.registerHotKeys,
                onDestruct: $scope.hideWnd
            }
        });

        $scope.existingConfs    = $$("existingConfs" + $scope.Name);
        $scope.createNewConfBtn = $$("createNewConfBtn");
        $scope.confFilter       = $$(filter.EDITOR);
    } else {
        $scope.createNewConfBtn.disable();
        $scope.existingConfs.clearAll();
    }

    return view;
}
/**
 * Created by Gifer on 11.08.2016.
 */

function confManagerController($scope, $rootScope) {
    "use strict";

    $scope.Name = mcConst.dataModels.ConfManager;

    var viewConfManager  = null;
    var ZERO_CONF_RIGHTS = '0011101100';
    var confList = {};

    var PRESET_ENTER_PWD = 'preset_enter_pwd';

    $scope._rights = {
        QTxtChCreate: false,
        QTxtChJoin: false
    };

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

    $scope.registerHotKeys = function () {
        $rootScope.hotKeyDispatcher.addPreset($scope.Name, [{
            key   : mcConst.keyCodes.esc,
            func  : function () {
                if ($scope.confFilter.getValue() === ""){
                    $scope.hideWnd();
                } else {
                    $scope.confFilter.$setValue("");
                }
            }
        }, {
            key  : mcConst.keyCodes.enter,
            func : function () {
                $scope.openOrCreate();
                
                $scope.confFilter.$setValue("");
            }
        }], document, true);
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset($scope.Name);
    };

    $scope.createNewConf = function(name){
        $rootScope.$broadcast('SendCMDToServer', [
            mcConst.lockInterface,
            mcConst._CMD_.cs_create_txt_channel,
            mcConst.SessionID,

            name,
            '',
            '',
            mcConst._CMD_.confType.STANDARD,
            '',

            function (info) {
                $scope.openConf(info.UID, null, info.Secured);
            }
        ]);
    };

    $scope.openOrCreate = function(){
        if (!$scope.confName){
            $scope.confName = $scope.confFilter.getValue();
        }

        var id = mcService.findItemInArrayOfObj(confList, $scope.confName, 'Name');

        if (id === -1){
            if ($scope._rights.QTxtChCreate) {
                $scope.createNewConf($scope.confName);
            } else {
                $rootScope.$broadcast('ErrorMsg', [7, mcService.Error(7)]); // "7"  : "нет прав для создания текстового канала", mcConst.ErrorText
            }
        } else
        if ($scope._rights.QTxtChJoin){
            $scope.openConf(confList[id].id, null, confList[id].Secured);
        } else {
            $rootScope.$broadcast('ErrorMsg', [8, mcService.Error(8)]); // "8"  : "нет прав для подключения к текстовому каналу",
        }

        $scope.hideWnd();
    };

    $scope.passEnter = function () {
        var pwdWindow = $$("confPassword" + $scope.Name);

        if (pwdWindow){
            pwdWindow.destructor();
        }

        pwdWindow = webix.ui({
            id      : "confPassword" + $scope.Name,
            view    : "window",
            modal   : true,
            position: 'center',
            head    : false,
            body    : { rows: [
                { view: "text", type: "password", placeholder: mcLang(2)}, // "2"  : "Пароль",
                { cols: [
                    { view: "button", value: mcLang(5), click: function() { // "5"  : "Войти",
                        $scope.openConf($scope.selectedConf, pwdWindow.getChildViews()[0].getChildViews()[0].getValue());

                        pwdWindow.closeWnd();
                    }},
                    { view: "button", value: mcLang(33), click: function(){ // "33" : "Отмена",
                        pwdWindow.closeWnd();
                    }}
                ]}
            ]},
            on: {
                onShow: function () {
                    var edit = pwdWindow.getChildViews()[0].getChildViews()[0];

                    $rootScope.hotKeyDispatcher.addPreset(PRESET_ENTER_PWD, [{
                        key: mcConst.keyCodes.enter,
                        func: function () {
                            var id   = mcService.findItemInArrayOfObj(confList, $scope.confName, 'Name');
                            var UID  = -1;
                            var password = edit.getValue();

                            if (id) {
                                UID = $scope.selectedConf || confList[id].id;
                            }

                            if (id && password){
                                $scope.openConf(UID, password);

                                pwdWindow.closeWnd();
                            }
                        }
                    }, {
                        key: mcConst.keyCodes.esc,
                        func: function () {
                            pwdWindow.closeWnd();
                        }
                    }], document, true);

                    edit.focus();
                },
                onDestruct: function () {
                    $rootScope.hotKeyDispatcher.removePreset(PRESET_ENTER_PWD);
                }
            }
        });

        pwdWindow.closeWnd = function () {
            pwdWindow.hide();
            pwdWindow.getChildViews()[0].getChildViews()[0].setValue('');
            pwdWindow.destructor();
        };

        pwdWindow.show();

        return pwdWindow;
    };

    $scope.openConf = function(uid, pass, secure){
        var UID = 'dUID-' + uid;

        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.ChatFrame, UID]);
        
        if (secure && !confList[UID]) {
            $rootScope.$broadcast("isConfOpen", [UID, function (here) {
                if (here){
                    $scope.hideWnd();

                    $rootScope.$broadcast('OpenConf', [uid]);
                } else {
                    $scope.passEnter();
                }
            }]);
        } else {
            $scope.hideWnd();

            $rootScope.$broadcast('OpenConf', [uid, pass]);
        }
    };

    $scope.getData = function(){
        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_get_channels_list,
            mcConst.SessionID,
            function(data){
                confList = mcService.convertObjToArray(data, 'id');

                $scope.existingConfs.parse(confList);
            }
        ]);
    };

    $scope. hideWnd = function () {
        $scope.removeHotKeys();
        
        viewConfManager.hide();

        var cType = $rootScope.GetChatType();
        
        if (cType === "UID" || cType === "UIN") {
            $rootScope.$broadcast(window._messages_.chatFrame.focusToEnterChat);
        }
    };

    $scope.show = function(){
        viewConfManager = initConfManagerView($scope);

        viewConfManager.show();

        setTimeout(function () {
            $scope.confFilter.focus();
        }, 800);
    };

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

    $scope.$on('hide' + $scope.Name, $scope.hideWnd);

    $scope.$on('show' + $scope.Name, function(){
        $scope.wndSize = $rootScope.wndSize;

        $scope.show();
        $scope.getData();
    });

    $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
        $scope._rights.QTxtChCreate = mcService.checkRights(mcConst._CMD_.RS.QTxtChCreate);
        $scope._rights.QTxtChJoin   = mcService.checkRights(mcConst._CMD_.RS.QTxtChJoin);
    });
}


function initHistoryBroadcastView($scope){
    "use strict";

    var view = $$($scope.Name);

    if (!view){
        var endOfTheDay = new Date();
        endOfTheDay.setHours(23);
        endOfTheDay.setMinutes(55);
        
        var filter = new mcService.FilterFiled({
            listName     : "sbrStructList",
            css          : "noBorderAll",
            noClearBtn   : true,
            placeholder  : mcLang(48), // "48" :"Поиск...",
            filterParams : ['DisplayName']
        });

        $$($scope.container).addView({
            id      : $scope.Name,
            padding : 0,
            css     : "whiteBG",
            rows    : [
                { id: "title" + $scope.Name, view: "template", template: mcLang(665), autoheight: true, borderless: true, height: 34, css: "byCenter header"}, // "665":"История оповещений",

                { rows: [
                    { cols: [
                        { id: "switchView" + $scope.Name, width: 300, view: "richselect", css: "byCenterAll noBorderAll finger", value: 1, options: [
                            { id: "1", value: mcLang(666)}, // "666":"Отправленные",
                            { id: "2", value: mcLang(667)}  // "667":"Принятые",
                        ]},

                        $scope.dwlMngr.progressWrapper,

                        { gravity: 0 }
                    ]},

                    { id: "historySended" + $scope.Name, cols: [
                        { width: 300, maxWidth: 500, minWidth: 200, rows: [
                            { height: 1, css: "borderBottom"},
                            { height: 2},

                            {   id          : "sendedList" + $scope.Name,
                                view        : "datatable",
                                select      : "row",
                                css         : "paddingCell3",
                                scroll      : "y",
                                header      : false,
                                minColumnWidth : 70,
                                navigation  : false,
                                columns     : [
                                    { id:"UIN",         header:"UIN", width: 55,     sort:"int"},
                                    { id:"DisplayName", header:mcService.Lang(1895), sort:"string", fillspace: true}, // "1895":"Отображаемое имя",
                                ],
                                on: {
                                    onAfterSelect : $scope.selectReceive,
                                    onItemDblClick: $scope.openPrivateFromBroadcast
                                }
                            }
                        ]},

                        { view: "resizer", css: "noBorder"},

                        { id: "messagesSended" + $scope.Name, template: "&nbsp;", css: "selectable scrollMenu grid_area", padding: 0,  scroll: true, borderless: true}
                    ]},

                    { id: "historyReceived" + $scope.Name, cols: [
                        { width: 300, maxWidth: 500, minWidth: 200, rows: [
                            { height: 1, css: "borderBottom"},
                            { height: 2},

                            {   id          : "receiveList" + $scope.Name,
                                view        : "datatable",
                                select      : "row",
                                css         : "paddingCell3",
                                scroll      : "y",
                                header      : false,
                                minColumnWidth : 70,
                                navigation  : false,
                                columns     : [
                                    { id:"UIN",         header:"UIN", width: 55,     sort:"int"},
                                    { id:"DisplayName", header:mcService.Lang(1895), sort:"string", fillspace: true}, // "1895":"Отображаемое имя",
                                ],
                                on: {
                                    onAfterSelect : $scope.selectReceive,
                                    onItemDblClick: $scope.openPrivateFromBroadcast
                                }
                            }
                        ]},

                        { view: "resizer", css: "noBorder"},

                        { id: "messagesReceived" + $scope.Name, template: "&nbsp;", css: "selectable scrollMenu grid_area", padding: 0,  scroll: true, borderless: true}
                    ]}
                ]}
            ]}
        );

        view = $$($scope.Name);

        $scope.switchView       = $$("switchView" + $scope.Name);
        $scope.messagesReceived = $$("messagesReceived" + $scope.Name);
        $scope.messagesSended   = $$("messagesSended" + $scope.Name);

        $scope.receiveList      = $$("receiveList" + $scope.Name);
        $scope.sendedList       = $$("sendedList" + $scope.Name);
        
        $scope.historySended    = $$("historySended" + $scope.Name);
        $scope.historyReceived  = $$("historyReceived" + $scope.Name);

        $scope.switchView.getList().define("template", function (obj) {
            return "<div class='byCenter'>" + obj.value + "</div>";
        });
        $scope.switchView.getList().refresh();

        $scope.initDU();
    } else {
        view.show();

        $scope.messagesReceived.define("template", "&nbsp;");
        $scope.messagesReceived.refresh();
    }

    return view;
}

function historyBroadcastController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.HistoryBroadcast;

    var view          = null;
    var selectedUsers = null;
    var duManager     = null;
    var selectTimer   = null;
    var restoreTool   = null;
    var recList       = {};

    window._broadcastActions.openOrDownloadFile = function (e){
        var url = this.href;
        var fileInfo = {
            uin        : this.getAttribute("uin"),
            displayName: this.getAttribute("nick"),
            filePath   : this.getAttribute("filepath"),
            fileDT     : this.getAttribute("filedt"),
            hash       : this.getAttribute("hash")
        };

        if ($rootScope.isWebClient){
            // window.open(url);
        } else {
            e.preventDefault();

            duManager.openDownloadUrl(url, fileInfo);
        }

        return false;
    };

    window._broadcastActions.downloadFile  = function (){
        var link = this.parentElement.childNodes[1];
        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 {
            duManager.downloadUrl(url, fileInfo);
        }
    };

    window._broadcastActions.openInFolder  = function (){
        var hash = this.parentElement.childNodes[1].getAttribute('hash');
        var uin  = this.parentElement.childNodes[1].getAttribute('uin');

        duManager.openInFolder(hash, uin);
    };

    function close(){
        if (view && view.isVisible()) {
            $scope.removeHotKeys();

            view.hide();
        }
    }

    function drawFile(fileList, UIN, DisplayName) {
        var res = "";

        if (fileList && fileList.length){
            fileList.forEach(function (_file) {
                var filePath = ""; //_file.info[1];
                var fileDT   = _file.UTCWriteTime;
                var fileSize = _file.Size;
                var fileName = _file.OriginalFileName;
                var text     =  mcService.myReplaceFormated(
                    // icon
                    '<span><i class="fa fa-file-o" aria-hidden="true"></i>' +

                    // file name
                    '<a id="link_mcService.extractFileName#{hash}" ' +
                    '   hash="#{hash}" ' +
                    // '   filepath="#{filepath}" ' +
                    '   filepath="" ' +
                    '   filedt="#{filedt}" ' +
                    '   filesize="#{filesize}" ' +
                    '   href="#{href}" ' +
                    '   nick="#{nick}" ' +
                    '   uin="#{uin}" ' +
                    '   target="_blank" ' +
                    '   onclick="window._broadcastActions.openOrDownloadFile.apply(this, arguments)" ' +
                    '   class="font90 fileLink"' +
                    '>#{file}</a>&nbsp;&nbsp;' +

                    // download icon
                    '<i class="fa fa-download finger" aria-hidden="true" title="#{dTitle}" onclick="window._broadcastActions.downloadFile.apply(this)"></i>&nbsp;' +

                    // open folder icon
                    ($rootScope.isWebClient ? '' : '<i class="fa fa-folder-open-o finger" onclick="window._broadcastActions.openInFolder.apply(this, arguments)" title="#{oTitle}" aria-hidden="true"></i>&nbsp;') +

                    // file size
                    '<span class="font90">(#{formatedSize})</span></span>',
                    {
                        nick    : DisplayName,
                        uin     : UIN,
                        hash    : _file.Hash,
                        dTitle  : mcLang(573), // "573":"Скачать",
                        oTitle  : mcLang(574), // "574":"Показать в папке",
                        href    : mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasFiles + "/" + _file.Hash + "/" + encodeURI(fileName),
                        file    : fileName,
                        filepath: filePath,
                        filedt  : fileDT,
                        filesize: fileSize,
                        formatedSize: mcService.formatFileSize(fileSize)
                    }
                );

                res += "<br>" + text;
            });
        }

        return res;
    }

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

    $scope.openPrivateFromBroadcast = function(item){
        var user = $scope.receiveList.getItem(item.row);

        if (user){
            $rootScope.$broadcast(window._messages_.chatWrapper.showHideMenu, false);
            $rootScope.$broadcast(window._messages_.dialogsList.OpenPrivate, [user.UIN]);
        }
    };

    $scope.close = close;

    $scope.registerHotKeys = function () {
        $rootScope.hotKeyDispatcher.addPreset(mcConst.dataModels.HistoryBroadcast, [{
            key   : mcConst.keyCodes.esc,
            lockPrev: true,
            func  : function () {
                switch ($rootScope.GetChatType()) {
                    case $rootScope.chatAliases.UID:
                    case $rootScope.chatAliases.UIN:
                        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.ChatFrame, 'd' + $rootScope.GetChatTypeID()]);
                    break;

                    default:
                        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [restoreTool]);
                }

                restoreTool = null;
            }
        }], document, true);
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset(mcConst.dataModels.HistoryBroadcast);
    };

    $scope.getData = function () {
        var today = new Date();

        $rootScope.$broadcast('SendCMDToServer' , [
            mcConst.lockInterface,
            mcConst._CMD_.cs_get_broadcasts_history,
            mcConst.SessionID,

            2,                                                  // ActiveState, статус оповещения. 0 - активное, 1 - "протухшее", 2 - не важно, какое
            mcService.formatDate(new Date(today.getTime() - mcConst.oneDay * 365), "dd.mm.yyyy.hh.nn.ss"), // dtTo, конец периода
            mcService.formatDate(today, "dd.mm.yyyy.hh.nn.ss"), // dtFrom, период, с которого надо получить оповещения, начало

            function (data) {
                // "Active"     : true,                   // тип оповещения (true - активное, false - устаревшее, из архива)
                // "Msg"        : "Hello!",               // текст сообщения
                // "ID"         : "",                     // идентификатор оповещения, число
                // "dtCreated"  : "dd.mm.yyyy.hh.nn.ss",  // дата создания оповещения (UTC)
                // "dtExpired"  : "dd.mm.yyyy.hh.nn.ss",  // конечная дата актуальности оповещения (UTC)
                // "Importance" : true,                   // "важное" оповещение
                // "WantRead"   : false                   // обязательно к прочтению

                var uList = [];

                recList = {};

                if (data && data.length){
                    data.forEach(function (msg) {
                        var users = msg.Users.split(',');

                        users.forEach(function (uin) {
                            if (!recList[uin]) {
                                recList[uin] = [];

                                uList.push(uin);
                            }

                            recList[uin].push(msg)
                        });
                    });

                    // $rootScope.$broadcast(window._messages_.usersSmallInfo.getInfoByUserUINs, [uList, function () {
                        uList.forEach(function (uin, id) {
                            uList[id] = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [uin]);
                            uList[id].Count = recList[uin].length;
                        });

                        $scope.show(uList);
                    // }]);
                } else {
                    $scope.show(uList);
                }
            }
        ]);
    };

    $scope.selectReceive = function(row){
        var user = $scope.receiveList.getItem(row.id);
        var uin  = user.UIN;
        var res  = [];

        recList[uin].forEach(function (message) {
            var text = message.Msg.replace('L$', "").split('\n').join("<br>");

            text = mcService.replaceMcComponents(text, $rootScope.isWebClient, $rootScope);

            text = mcService.ReplaceURLs(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 + '">';
                }

                return link ? '<a href="' + link + '" title="' + link + '" target="_blank">' + (res || text) + '</a>' : text;
            }});



            res.push(mcService.myReplaceFormated(
                "<div class='grid_area_bst'>" +
                    "<div class='gray font90 paddingTop7 byRight' style='margin-right: 15px'>#{time} - #{timeEnd}</div>" +
                    "<div class='borderBottom lHeight26 framePaddingAll10'>#{text}#{fileList}</div>" +
                "</div>",
                {
                    text    : text,
                    time    : mcService.formatDate(mcService.utcTime(message.dtCreatedUTC), "dd MMM yyyy, hh:nn"),
                    timeEnd : mcService.formatDate(mcService.utcTime(message.dtExpiredUTC), "dd MMM yyyy, hh:nn"),
                    fileList: drawFile(message.Files, uin, user.DisplayName)
                }
            ));
        });

        $scope.messagesReceived.define("template", "<br>" + res.join("") + "<br>");
        $scope.messagesReceived.refresh();
    };

    $scope.show = function(uList){
        $scope.registerHotKeys();

        if (!duManager){
            duManager = new DownloadUploadManager($rootScope, {
                name: $scope.Name,
                download: { enable: true }
            });

            $scope.initDU     = duManager.initViews;
            $scope.dwlMngr    = duManager.getViews(mcLang(33)); // "33" :"Отмена",
        }

        view = initHistoryBroadcastView($scope);

        $scope.receiveList.clearAll();
        $scope.receiveList.parse(uList);
    };

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

    $scope.$on('hide' + $scope.Name, function(){
        close();
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        restoreTool = $rootScope.GetChatType();

        if (mcService.checkNoRestoreTool(restoreTool)){
            restoreTool = null;
        }

        $scope.getData();
    });
}
function initBroadcastView($scope, broadcasts){
    "use strict";

    var view = $$($scope.Name);

    // Avatar      : 0, 
    // DisplayName : "Andrey Rakov",
    // ID          : 428,
    // Sex         : 1,
    // State       : 0,
    // dtCreatedUTC: "22.06.2018.11.06.53",
    // files_list  : [],
    // msg         : "sdfsdf",
    // msg_type    : 0,
    // read_notify : true,
    // uinOwner    : 3

    if (!view){
        view = webix.ui({
            id      : $scope.Name,
            view    : "window",
            position: "center",
            modal   : true,
            width   : document.body.offsetWidth  - 40,
            height  : document.body.offsetHeight - 40,
            head    : false,
            body    : { padding: 5, rows: [
                { id: "brTitle", template: "&nbsp;", height: 38, css: "byCenter lHeight36"},

                { id: "brCntWrapper", cols: [
                    { id: "brUserList", view: "list", width: 200, select: true, minWidth: 200, maxWidth: 500, 
                        type:{
                            height: 50
                        },
                        template: function (obj) {
                            return  mcService.myReplaceFormated(
                                "<div class='lHeight26'>" +
                                    "<div class='firstLineList'>#{name}</div>" +
                                    "<div>#{date}#{onlySee}#{confirm}#{reply}</div>" +
                                "</div>",
                                {
                                    name   : obj.DisplayName,
                                    date   : mcService.formatDate(mcService.utcTime(obj.dtCreatedUTC), "hh:nn dd.mm.yyyy"),
                                    onlySee: obj.msg_type === $scope.msgType.regular    ? " " + mcService.insertIco("fa-eye", " ") : "",
                                    confirm: obj.msg_type === $scope.msgType.needRead   ? " " + mcService.insertIco("fa-check-square-o", " ") : "",
                                    reply  : obj.msg_type === $scope.msgType.needAnswer ? " " + mcService.insertIco("fa-comments-o", " ") : ""
                                });
                        },
                        on: {
                            onAfterSelect : $scope.selectBroadcast,
                            onBeforeSelect: $scope.backupAnswer
                        }
                    },

                    { view: "resizer", borderless: true },

                    { rows: [
                        { cols: [
                            { rows: [
                                $scope.dwlMngr.progressWrapper,

                                {cols:[
                                    { id: "brMsgContent", template: "&nbsp;", css: "framePaddingAll10 selectable", padding: 5},
                                    { width: 15}
                                ]},

                                { id: "brAnswerResizer", view: "resizer", hidden: true},

                                { id: "brAnswer", view: "textarea", css: "noBGColorAll", hidden: true, height: 60, placeholder: mcLang(621)}, // "621":"Введите здесь ответ на оповещение",

                                { rows: [
                                    { id: "brImReadMsgWrap", cols: [
                                        { id: "brImReadMsg", view: "checkbox", value: false, labelWidth: 0, labelRight: mcLang(607),  css: "finger", // "607":"Я прочитал сообщение",
                                            on: {onChange: $scope.readMsg}
                                        },
                                        {},
                                        { id: "brCloseWnd", view: "button", width: 150, value: mcLang(12), click: $scope.closeBroadcast} // "12" :"Закрыть",
                                    ]},

                                    { height: 20 }
                                ]}
                            ]},

                            { id: "brFilesResizer", view: "resizer"},

                            { id: "brFileListWrap", hidden: true, rows: [
                                { height: 3},

                                { id: "brFilesCount", template: mcLang(622), css: "byCenter", autoheight: true, borderless: true}, // "622":"Файлы",

                                { height: 3},

                                { id: "brFileList", view: "list", width: 200, select: false, minWidth: 200, maxWidth: 500, css: "font90 lineHeight24 filesList noPaddingList", // lHeight17All
                                    type: { height: "auto" },
                                    template: function (obj) {
                                        return mcService.myReplaceFormated(
                                            "<div class='bolder'>#{fName}</div><div class='gray'>#{sizeTitle}: #{size}</div>",
                                            {
                                                fName    : obj.OriginalFileName,
                                                size     : mcService.formatFileSize(obj.Size),
                                                sizeTitle: mcLang(590) // "590":"размер",
                                            }
                                        );
                                    },
                                    on: {
                                        onItemClick: $scope.downloadFile
                                    }
                                }
                            ]}
                        ]}
                    ]}
                ]}
            ]}
        });

        $scope.initDU();
    }

    broadcasts.forEach(function (itm) {
        itm.id = itm.ID;
    });

    $scope.brUserList      = $$("brUserList");
    $scope.brTitle         = $$("brTitle");
    $scope.brMsgContent    = $$("brMsgContent");
    $scope.brImReadMsgWrap = $$("brImReadMsgWrap");
    $scope.brImReadMsg     = $$("brImReadMsg");
    $scope.brCntWrapper    = $$("brCntWrapper");
    $scope.answerResizer   = $$("brAnswerResizer");
    $scope.answer          = $$("brAnswer");
    $scope.brFileListWrap  = $$("brFileListWrap");
    $scope.brFilesResizer  = $$("brFilesResizer");
    $scope.brFileList      = $$("brFileList");
    $scope.brFilesCount    = $$("brFilesCount");

    $scope.brUserList.parse(broadcasts);
    $scope.brUserList.select($scope.brUserList.getFirstId());

    view.show();

    return view;
}
function broadcastController($scope, $rootScope, mcPlaySound){
    "use strict";

    $scope.Name = mcConst.dataModels.Broadcast;

    var view               = null;
    // var deferredLoadImages = new McDeferredLoadImages();
    var currentBrID        = null;
    var readBroadcasts     = {};
    var broadcasts         = [];
    var canChangeCkb       = true;
    var downloadUploadManager = null;
    var stateBackup        = null; // main window state: visible, focus, minimize

    $scope.msgType = {
        regular   : 0, // 0 - обычное оповещение,
        needRead  : 1, // 1 - оповещение обязательно к прочтению. Окошко у получателя не закроется, пока тот не подтвердит, что он действительно прочитал сообщение
        needAnswer: 2  // 2 - оповещение требует специального ответа от получателя, и не закроется, пока тот не даст ответ
    };

    function hideBroadcastView(){
        if (view && view.isVisible()) {
            view.hide();

            downloadUploadManager && downloadUploadManager.noStart();
        }

        $scope.removeHotKeys();

        broadcasts     = [];
        readBroadcasts = {};
        currentBrID    = null;
        stateBackup    = null;

        $scope.brUserList && $scope.brUserList.clearAll();

        if (!$rootScope.isWebClient){
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_disable_always_on_top
            ]);
        }
    }

    function close() {
        var list     = mcService.ObjToString(readBroadcasts);
        var canClose = true;
        var notifyList = {};

        broadcasts.forEach(function (item) {
            if (!((item.msg_type === $scope.msgType.needRead   && readBroadcasts[item.ID] && readBroadcasts[item.ID].read) ||
                  (item.msg_type === $scope.msgType.needAnswer && readBroadcasts[item.ID] && readBroadcasts[item.ID].ReplyText) ||
                  (item.msg_type === $scope.msgType.regular    && readBroadcasts[item.ID])
                )) {
                
                $rootScope.$broadcast('ErrorMsg', [-1, mcLang(612), "", function(){ // "612":"Вы не прочитала все оповещения или не ответили",
                    
                }]);

                canClose = false;
            }

            if (item.read_notify){
                notifyList[item.ID] = {
                    UIN: item.uinOwner
                };
            }
        });

        if (canClose && $scope.brUserList){
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_broadcast_just_readed,
                mcConst.SessionID,

                list
            ]);

            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_broadcast_readed_notify,
                mcConst.SessionID,

                mcService.ObjToString(notifyList)
            ]);

            hideBroadcastView();
        } else {

        }
    }

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

    $scope.registerHotKeys = function (){
        $rootScope.hotKeyDispatcher.disable(); //addPreset("_keys_" + $scope.Name, [], document, true); // disable all hotkeys
    };

    $scope.removeHotKeys = function (){
        $rootScope.hotKeyDispatcher.enable(); //removePreset("_keys_" + $scope.Name);
    };

    $scope.readMsg = function(val){
        if (currentBrID && canChangeCkb){
            readBroadcasts[currentBrID].read = mcService.convertIntToBool(val);
        }
    };

    $scope.closeBroadcast = function () {
        var item = $scope.brUserList.getItem(currentBrID);

        if (readBroadcasts[currentBrID] && item && item.msg_type === $scope.msgType.needAnswer){
            readBroadcasts[currentBrID].ReplyText = $scope.answer.getValue();
        }

        close();
    };

    $scope.downloadFile = function(id){
        var item = $scope.brFileList.getItem(id);

        if (item && item.Hash){
            var broadcastItem = broadcasts[mcService.findItemInArrayOfObj(broadcasts, currentBrID, "ID")];
            var url           = mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasFiles + "/" + item.Hash + "/" + encodeURI(item.OriginalFileName);
            var fileInfo      = {
                uin        : broadcastItem.uinOwner,
                displayName: broadcastItem.DisplayName,
                filePath   : "",
                fileDT     : item.UTCWriteTime,
                hash       : item.Hash
            };

            if ($rootScope.isWebClient){
                window.open(url);
            } else {
                downloadUploadManager.openDownloadUrl(url, fileInfo);
            }
        }
    };

    $scope.selectBroadcast = function (id) {
        var item = $scope.brUserList.getItem(id);

        canChangeCkb = false;
        currentBrID  = id;

        var text = item.msg.split(mcConst.CRLF).join("<br>");

        text = mcService.replaceMcComponents(text, $rootScope.isWebClient, $rootScope);

        text = mcService.ReplaceURLs(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 + '">';
            }

            return link ? '<a href="' + link + '" title="' + link + '" target="_blank">' + (res || text) + '</a>' : text;
        }});

        $scope.brTitle     .define("template", mcLang(611) + item.DisplayName + " <span class='Gray monospaceAll'>" + mcService.formatDate(mcService.utcTime(item.dtCreatedUTC), "[hh:nn dd.mm.yyyy]") + "</span>"); // "611":"Оповещение от: ",
        $scope.brMsgContent.define("template", text);

        $scope.brTitle     .refresh();
        $scope.brMsgContent.refresh();

        $scope.brImReadMsg   [item.msg_type === $scope.msgType.needRead   ? "show" : "hide"]();
        $scope.answerResizer [item.msg_type === $scope.msgType.needAnswer ? "show" : "hide"]();
        $scope.answer        [item.msg_type === $scope.msgType.needAnswer ? "show" : "hide"]();
        $scope.brFileListWrap[item.files_list && item.files_list.length ? "show" : "hide"]();
        $scope.brFilesResizer[item.files_list && item.files_list.length ? "show" : "hide"]();

        $scope.brFileList.clearAll();
        $scope.currentFilesCount = 0;

        if (item.files_list){
            $scope.brFileList.parse(item.files_list);
            $scope.currentFilesCount = item.files_list.length;

            $scope.brFilesCount.define("template", mcLang(622) + " [" + $scope.currentFilesCount + "]");
            $scope.brFilesCount.refresh();
        }

        if (!readBroadcasts.hasOwnProperty(currentBrID)){
            readBroadcasts[currentBrID] = {
                ReplyText: "",
            };
        }

        if (item.msg_type === $scope.msgType.needRead) {
            $scope.brImReadMsg.setValue(readBroadcasts[currentBrID].read);
        } else
        if (item.msg_type === $scope.msgType.needAnswer){
            $scope.answer.setValue(readBroadcasts[currentBrID].ReplyText);
        }

        canChangeCkb = true;
    };

    $scope.backupAnswer = function () {
        var item = $scope.brUserList.getItem(currentBrID);

        if (readBroadcasts[currentBrID] && item && item.msg_type === $scope.msgType.needAnswer){
            readBroadcasts[currentBrID].ReplyText = $scope.answer.getValue();

            $scope.answer.setValue("");
        }
    };

    $scope.show = function(_broadcasts){
        broadcasts = _broadcasts;

        if (!downloadUploadManager){
            downloadUploadManager = new DownloadUploadManager($rootScope, {
                name: $scope.Name,
                download: {enable: true}
            });

            $scope.initDU     = downloadUploadManager.initViews;
            $scope.dwlMngr    = downloadUploadManager.getViews(mcLang(33)); // "33" :"Отмена",
        }

        view = initBroadcastView($scope, broadcasts);

        downloadUploadManager.canStart();
    };

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

    var _msg = window._messages_.Broadcast = {
        onReceiveBroadcast    : 'onReceiveBroadcast',
        closeReceiveBroadcast : 'closeReceiveBroadcast'
    };

    $scope.$on(_msg.onReceiveBroadcast, function (e, args) {
        if (!stateBackup){
            stateBackup = mcService.Marge({}, mcConst.mainWindowSate);

            $scope.registerHotKeys();
        }

        $scope.show.apply(null, args);
    });

    $scope.$on(_msg.closeReceiveBroadcast, function () {
        if (stateBackup && !$rootScope.isWebClient){
            $rootScope.$broadcast('sendCMDToElectron', [
                mcConst._CMD_.ce_set_window_state,
                stateBackup
            ]);
        }

        hideBroadcastView();
    });

    $scope.$on('hide' + $scope.Name, function(){
        close();
    });
}


function initSendBroadcastView($scope){
    "use strict";

    var view = $$($scope.Name);

    function icon(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>";
    }

    if (!view){
        var endOfTheDay = new Date();
        endOfTheDay.setHours(23);
        endOfTheDay.setMinutes(55);
        
        var filter = new mcService.FilterFiled({
            listName     : "sbrStructList",
            css          : "noBorderAll",
            noClearBtn   : true,
            placeholder  : mcLang(48), // "48" :"Поиск...",
            filterParams : ['DisplayName']
        });

        $$($scope.container).addView({
            id      : $scope.Name,
            css     : "whiteBG",
            rows    : [
                { height: 37, cols: [
                    { id: "sbrTitle",  template: mcLang(623), css: "byCenter lHeight36", borderless: true }, // "623":"Отправить оповещение",
                    { view: "button", type: "icon", icon: "calendar", label: mcLang(665), hidden: true, width: 220, css: "noUpperCase", click: $scope.openBrcHistory} // "665":"История оповещений",
                ]},

                $scope.dwlMngr.progressWrapper,

                { cols: [
                    { width: 5},
                        
                    { rows: [
                        { id: "sbrMessageType"  + $scope.Name, view: "richselect",
                            options: [
                                { id: "0", value: mcLang(634)}, // "634":"Стандартное оповещение",
                                { id: "1", value: mcLang(635)}, // "635":"Обязательно к прочтению",
                                { id: "2", value: mcLang(636)}  // "636":"Требуется ответ",
                            ],
                            value: 1,
                            // on: {
                                // onItemClick: $scope.checkEmptyList,
                                // onChange   : $scope.changeServerHost
                            // }
                        },

                        { cols: [
                            { view:"datepicker",
                                id           : "sbrExpireDate" + $scope.Name,
                                value        : new Date(),
                                labelPosition: 'left',
                                labelAlign   : 'right',
                                label        : mcLang(633), // "633":"Актуально до:",
                                labelWidth   : 110,
                                width        : 240,
                                suggest      : {
                                    type : "calendar",
                                    body : { minDate: new Date((new Date()).getTime() - mcConst.oneDay), maxDate: new Date("3000.01.01") }
                                }
                            },
                            { view:"datepicker",
                                id           : "sbrExpireTime" + $scope.Name,
                                value        : endOfTheDay,
                                labelWidth   : 0,
                                width        : 85,
                                format       : "%H:%i",
                                icon         : "clock-o",
                                suggest      : {
                                    type:"calendar",
                                    body:{
                                        type:"time",
                                        calendarTime:"%H:%i"
                                    }
                                }
                            },
                            {},
                            { gravity: 0}
                        ]},

                        { id: "sbrShowOnlySelected", view: "checkbox", value: false, labelRight: mcLang(630), labelWidth: 0, click: $scope.showOnlySelectedUsers }, // "630":"Показать только выбрнных", "631":"Показать всех",
                        // { view: "toggle", value: 0, offLabel: mcLang(630), onLabel: mcLang(631),  click: $scope.showOnlySelectedUsers }, // "630":"Показать только выбрнных", "631":"Показать всех",

                        { id: "sbrNotifyForMe",  view: "checkbox", labelRight: mcLang(637), labelWidth: 0, value: false, css: "finger"}, // "637":"Уведомлять меня о прочтении",

                        { rows: [
                            { id: "sbrText", view: "textarea", placeholder: mcLang(629), css: "borderBottomLeft noBGAll"}, // "629":"Текст оповещения",

                            { height: 3},

                            { id: "sbrFilesCount", template: mcLang(622), css: "byCenter noBG", height: 25, hidden: true, borderless: true}, // "622":"Файлы",
                        ]},

                        { id: "sbrFileResizer", view: "resizer", css: "noBorder", hidden: true},

                        { id: "sbrFileList", view: "activeList", select: false, css: "lHeight17All font90 filesList delIcon noBG borderBottomLeft", hidden: true, height: 80,
                            activeContent:{
                                deleteButton: { view: "button", type: "icon", icon: "trash-o", width: 25, height: 25, css: "toRight lHeight20 noBG", click: $scope.removeFile,
                                    tooltip: mcLang(644) // "644":"Удалить элемент",
                                }
                            },
                            type:{
                                height: 25
                            },
                            template: function (obj, common) {
                                return mcService.myReplaceFormated(
                                    "<span class='bolder'>#{fName}</span> <span class='gray'>#{size}</span>" + common.deleteButton(obj, common),
                                    {
                                        fName    : obj.OriginalFileName,
                                        size     : mcService.formatFileSize(obj.Size)
                                    }
                                );
                            }
                        }
                    ]},

                    { view: "resizer", css: "noBorder noBGColorAll"},

                    { rows: [
                        filter,

                        {
                            view          : "treetable",
                            resizeColumn  : true,
                            id            : "sbrStructList",
                            select        : "row",
                            header        : false,
                            multiselect   : "touch",
                            navigation    : false,
                            scroll        : "y",
                            css           : "noBorderAll noWhiteSpace scrollMenu noBG borderBottomLeft paddingCell3",
                            fixedRowHeight: true,//false,
                            rowLineHeight :25,
                            rowHeight     :25,
                            columns       : [{
                                id      : "DisplayName",
                                template: function (obj, common) {
                                    var res  = "";
                                    var name = "";

                                    if (obj.folder) {
                                        var color = obj.$level > 1 ? "#00720d" : "black";

                                        name = (obj.$level > 1 ? mcService.insertIco("fa-users", "smaller") : "") + "<span class='treeBranchContacts webix_tree_item' style='color:" + color + ";'>" + obj.DisplayName + "</span>";
                                    } else

                                    if (obj.TeamLead) {
                                        name = "<span class='teamlead' style='color: rgb(12, 132, 228)'>" + mcService.insertIco("fa-user", "smaller") + obj.DisplayName + "</span>";
                                    } else {
                                        name = mcService.insertIco("fa-user", "smaller") + obj.DisplayName;
                                    }

                                    res = icon(obj) + "<span class='webix_tree_item ' style='cursor: pointer;'>" + name;

                                    if (!obj.folder) {
                                        res += " <span style='" + (mcService.isFuckingFirefox() ? "" : "float:right; ") + "color:#666; magrin-rigth: 19px; padding: 0; background: none; font-size: 85%'> uin: " + obj.UIN + "</span>";
                                    }

                                    res += "</span>";

                                    return common.space(obj) + res;
                                },
                                fillspace: true
                            }],
                            on: {
                                // onSubViewOpen : $scope.openBranch,
                                onSelectChange: $scope.usersSelect,
                                onBeforeSelect: function(selection, preserve){
                                    var branch = this.getItem(selection.id);

                                    if (branch.needLoad && !$scope.noOpeanBranch){
                                        branch.needLoad = false;

                                        this.updateItem(selection.id, branch);

                                        $scope.loadBranch(branch);
                                    }

                                    if (this.isBranch(selection.id) || branch.folder) {
                                        return false;
                                    }
                                },
                                onItemClick: function (item) {
                                    var id = item.row;

                                    if (this.isBranch(id) || this.getItem(id).folder) {
                                        if (this.isBranchOpen(id)){
                                            this.close(id);
                                        } else {
                                            this.open(id);
                                        }
                                    }
                                }
                            }
                        }
                    ]}
                ]},

                { css: "", cols: [
                    { gravity:1 },
                    { view: "button", value: mcLang(641), click: $scope.addFile, css: "", gravity: 4},        // "641":"Добавить файл",
                    { id: "sbrSendBroadcast", view: "button", disabled: true, value: mcLang(623), click: $scope.sendBroadcast, gravity: 4},  // "623":"Отправить оповещение",
                    { gravity:1 }
                ]}
            ]}
        );

        view = $$($scope.Name);

        $scope.sbrStructList         = $$("sbrStructList");
        $scope.sbrText               = $$("sbrText");
        $scope.sbrTitle              = $$("sbrTitle");

        $scope.sbrExpireDate         = $$("sbrExpireDate" + $scope.Name);
        $scope.sbrExpireTime         = $$("sbrExpireTime" + $scope.Name);
        $scope.sbrMessageType        = $$("sbrMessageType"  + $scope.Name);
        $scope.sbrNotifyForMe        = $$("sbrNotifyForMe");
        $scope.sbrShowOnlySelected   = $$("sbrShowOnlySelected");

        $scope.sbrFileResizer        = $$("sbrFileResizer");
        $scope.sbrFileList           = $$("sbrFileList");
        $scope.sbrFilesCount         = $$("sbrFilesCount");

        $scope.sbrSendBroadcast      = $$("sbrSendBroadcast");

        // $scope.sendWrapper           = $$("sendWrapper" + $scope.Name);
        // $scope.historyWrapper        = $$("historyWrapper" + $scope.Name);
        // $scope.switchView            = $$("switchView" + $scope.Name);

        $scope.sbrStructList.contextTitle = {
            all    : 638, // "638":"Выделить все",
            clear  : 639, // "639":"Снять выделение",
            invert : 640  // "640":"Инвертировать выделение",
        };

        new mcService.ContextMenu($scope.sbrStructList, null, $scope.contextBeforeActions, function () {
            // $scope.regShowWindow(this.config.id, this);
        });

        $scope.initDU();

    } else {
        view.show();
    }

    return view;
}
function sendBroadcastController($scope, $rootScope, mcPlaySound){
    "use strict";

    $scope.Name = mcConst.dataModels.SendBroadcast;

    var view              = null;
    var defaultGroupList  = [];
    var userTreeID        = 1;
    var selectedUsers     = null;
    var sendFiles         = [];
    var backupUserSetting = {
        state: null,
        list : null
    };
    var downloadUploadManager = null;
    var selectTimer       = null;

    var ONLINELIST   = "___online___";
    var ALLLIST      = "___all___";
    var COMMONLIST   = "___commonContacts___";
    var PERSONALLIST = "___personalContacts___";
    var CURRENTLIST  = "___currentContacts___";

    function close(){
        if (view && view.isVisible()) {
            $scope.removeHotKeys();

            view.hide();
        }

        downloadUploadManager && downloadUploadManager.noStart();
    }

    function convertToTree(data, startID){
        var res       = [];
        var structure = [];
        var _data     = [].concat(data);
        var i         = 0;
        var needAdd   = null;
        var _id       = startID || 1;

        function add(current, parent){
            var item = null;

            if (current.IsGroup){
                item = {
                    DisplayName: current.GroupName || current.DisplayName,
                    id         : _id,
                    // id         : current.ID || current.id,
                    ParentID   : current.ParentID,
                    GroupID    : current.GroupID,
                    IsGroup    : true,
                    folder     : true,
                    data       : current.data || []
                };
            } else {
                if (current.UIN == mcConst.UserInfo.UIN){
                    return false;
                }

                item = {
                    DisplayName: current.DisplayName,
                    id         : _id,
                    ParentID   : current.ParentID,
                    onlineState: current.onlineState,
                    UIN        : current.UIN,
                    TeamLead   : current.TeamLead
                };
            }

            _id ++;

            parent.push(item);

            return true;
        }

        while (_data.length > 0){
            if (_data[i].ParentID == -1){
                _data.splice(i, 1);
            } else
            // if (_data[i].ParentID == 0 || _data[i].$row === "DisplayName"){
            if (_data[i].ParentID == 0 || _data[i].$parent === 0){
                needAdd = add(_data[i], res);
                if (needAdd) add(res[res.length - 1], structure);

                _data.splice(i, 1);
            } else {
                var idx = mcService.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, _id];
    }

    function updateFilesCount() {
        $scope.sbrFilesCount.define("template", mcLang(622) + " [" + sendFiles.length + "]");
        $scope.sbrFilesCount.refresh();
    }

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

    $scope.openBrcHistory = function(){
        $rootScope.$broadcast(window._messages_.chatWrapper.showHideMenu);
        $rootScope.$broadcast(window._messages_.mainMenu.selectMenuElement, ["brcHistory"]);
    };

    $scope.contextBeforeActions = function (action) {
        switch (action) {
            case "beforeSelectAll":
                $scope.noOpeanBranch = true;
            break;

            case "selectAll":
                $scope.noOpeanBranch = false;
            break;

            case "beforeClear":
            break;
        }
    };

    $scope.makeDefaultList = function(){
        defaultGroupList = [
            {
                $parent: 0,
                $count: true,
                closed: true,
                needLoad: true,
                listName: ONLINELIST,
                open: false,
                folder: true,
                data: [],
                DisplayName: mcLang(624) // "624":"Все онлайн",
            },
            {
                $parent: 0,
                $count: true,
                closed: true,
                needLoad: true,
                listName: ALLLIST,
                open: false,
                folder: true,
                data: [],
                DisplayName: mcLang(625) // "625":"Все зарегистрированные",
            },
            {
                $parent: 0,
                listName: COMMONLIST,
                open: false,
                folder: true,
                data: [],
                DisplayName: mcLang(626) // "626":"Общий список контактов",
            },
            {
                $parent: 0,
                listName: PERSONALLIST,
                open: false,
                folder: true,
                data: [],
                DisplayName: mcLang(627) // "627":"Личный список контактов",
            },
            {
                $parent: 0,
                listName: CURRENTLIST,
                open: true,
                folder: true,
                data: [],
                DisplayName: mcLang(628) // "628":"Текущий список (конференция, приват)",
            }
        ];
    };

    $scope.loadBranch = function(branch){
        if (branch.listName === ONLINELIST || branch.listName === ALLLIST){
            // setTimeout(function () {
                $rootScope.$broadcast('SendCMDToServer' , [
                    mcConst._CMD_.cs_get_users_list,
                    mcConst.SessionID,

                    branch.listName === ONLINELIST ? 1 : 2, // online
                    1,
                    "",
                    -1,

                    function (data) {
                        data = mcService.convertObjToArray(data, "UIN");

                        data.forEach(function (item, i) {
                            data[i].DisplayName = data[i].Nick;
                            data[i].id  = userTreeID ++;
                            data[i].UIN = parseInt(data[i].UIN);
                        });

                        defaultGroupList = $scope.sbrStructList.data.serialize();
                        defaultGroupList[branch.listName === ONLINELIST ? 0 : 1].data = data;

                        // var state = $scope.sbrStructList.getState();

                        $scope.sbrStructList.parse(defaultGroupList);
                        // $scope.sbrStructList.setState(state);
                        $scope.sbrStructList.open(branch.id);
                    }
                ]);
            // }, 100);
        }
    };

    $scope.usersSelect = function(){
        if (!selectTimer){
            selectTimer = setTimeout(function () {
                selectedUsers = $scope.sbrStructList.getSelectedItem();

                if (selectedUsers && mcService.isObject(selectedUsers)){
                    selectedUsers = [selectedUsers];
                }

                if (selectedUsers && selectedUsers.length) {
                    var list = {};

                    selectedUsers.forEach(function (item) {
                        list[item.UIN] = item;
                    });

                    selectedUsers = mcService.convertObjToArray(list, "UIN");

                    // $scope.sbrTitle.define('template', mcLang(623) + " [" + selectedUsers.length + "]"); // "623":"Отправить оповещение",

                    $scope.sbrSendBroadcast.enable();
                } else {
                    // $scope.sbrTitle.define('template', mcLang(623)); // "623":"Отправить оповещение",

                    $scope.sbrSendBroadcast.disable();
                }

                // $scope.sbrTitle.refresh();

                selectTimer = null;
            }, 100);
        }
    };

    $scope.close = close;

    $scope.showOnlySelectedUsers = function(){
        if (!$scope.sbrShowOnlySelected.getValue() && backupUserSetting.list && backupUserSetting.state){
            var _select = backupUserSetting.state.select;

            $scope.sbrStructList.clearAll();
            $scope.sbrStructList.parse(backupUserSetting.list);
            $scope.sbrStructList.setState(backupUserSetting.state);

            backupUserSetting.list  = null;
            backupUserSetting.state = null;
        } else {
            var _usersBackup = selectedUsers;

            backupUserSetting.state = $scope.sbrStructList.getState();
            backupUserSetting.list  = $scope.sbrStructList.data.serialize();

            $scope.sbrStructList.clearAll();

            if (_usersBackup && _usersBackup.length){
                $scope.sbrStructList.parse(_usersBackup);
                $scope.sbrStructList.selectAll();
            }
        }
    };

    $scope.registerHotKeys = function () {
        $rootScope.hotKeyDispatcher.addPreset(mcConst.dataModels.SendBroadcast, [{
            key   : mcConst.keyCodes.enter,
            ctrlKey: true,
            func  : $scope.sendBroadcast
        }], $scope.sbrText.getInputNode());
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset(mcConst.dataModels.SendBroadcast);
    };

    $scope.sendBroadcast = function () {
        var msg = $scope.sbrText.getValue();

        if (selectedUsers && selectedUsers.length && msg){
            var users = selectedUsers.map(function (item) {
                return item.UIN;
            }).join(',');
            
            $rootScope.$broadcast('SendCMDToServer' , [
                mcConst._CMD_.cs_send_broadcast,
                mcConst.SessionID,

                JSON.stringify({
                    UsersList     : users,
                    ActualTo      : mcService.formatDate($scope.sbrExpireDate.getValue(), "dd.mm.yyyy.") + mcService.formatDate($scope.sbrExpireTime.getValue(), "hh.nn"),
                    MsgType       : $scope.sbrMessageType.getValue(),
                    ReadingNotify : mcService.convertIntToBool($scope.sbrNotifyForMe.getValue()),
                    // Msg           : "L$" + msg.split("\n").join("L$"),                             // todo: убрать это гавно "L$"
                    Msg           : msg.split("\n").join(mcConst.CRLF),
                    Files         : sendFiles
                }),

                function () {
                    $scope.sbrText.setValue("");
                    $scope.sbrFileList.clearAll();

                    $scope.sbrFileResizer.hide();
                    $scope.sbrFileList   .hide();
                    $scope.sbrFilesCount .hide();

                    sendFiles = [];

                    webix.alert(mcLang(642)); // "642":"Оповещение отправлено",
                }
            ]);
        } else
        if (!msg){
            webix.alert("<span class='red'>" + mcLang(645) + "</span>"); // "645":"Нет текста оповещения!",
        }
    };

    $scope.addFile = function(){
        $scope.uploadFile(mcConst._CMD_.msgType.FILE_NEW, false, false);
    };

    $scope.removeFile = function(){
        var id   = this.getNode().parentNode.getAttribute("webix_l_id");
        var item = $scope.sbrFileList.getItem(id);
        var idx  = mcService.findItemInArrayOfObj(sendFiles, item.id, "id");

        if (idx >=0) {
            sendFiles.splice(idx, 1);

            $scope.sbrFileList.remove(item.id);

            if (sendFiles.length){
                updateFilesCount();
            } else {
                $scope.sbrFileResizer.hide();
                $scope.sbrFileList   .hide();
                $scope.sbrFilesCount .hide();
            }
        }
    };

    $scope.getData = function () {
        var task = new mcService.TaskList();

        $scope.show();

        $scope.registerHotKeys();
        
        selectedUsers     = null;
        backupUserSetting = {
            state: null,
            list : null
        };
        sendFiles         = [];
        userTreeID        = 1;
        
        $scope.sbrStructList.unselectAll();
        $scope.sbrStructList.clearAll();
        $scope.sbrShowOnlySelected.setValue(false);
        $scope.sbrSendBroadcast.disable();

        $scope.makeDefaultList();

        task.AddTask(function () {
            if (mcConst.contactsCaches.PublicContacts){
                $rootScope.$broadcast('SendCMDToServer' , [
                    mcConst._CMD_.cs_get_common_contacts_list,
                    mcConst.SessionID,
                    function (data) {
                        data.forEach(function(contact){
                            if (contact.UIN !== -1) {
                                contact.DisplayName = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [contact.UIN]).DisplayName;
                            }
                        });

                        data = convertToTree(data, userTreeID);

                        defaultGroupList[2].data = data[0];
                        userTreeID               = data[1];

                        task.Next();
                    }
                ]);
            } else {
                task.Next();
            }
        });

        task.AddTask(function () {
            if (mcConst.contactsCaches.PersonalContacts){
                $rootScope.$broadcast('SendCMDToServer' , [
                    mcConst._CMD_.cs_get_contacts_list,
                    mcConst.SessionID,
                    function (data) {
                        data.forEach(function(contact){
                            if (contact.UIN !== -1) {
                                contact.DisplayName = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [contact.UIN]).DisplayName;
                            }
                        });
                        
                        data = convertToTree(data, userTreeID);

                        defaultGroupList[3].data = data[0];
                        userTreeID               = data[1];

                        task.Next();
                    }
                ]);
            } else {
                task.Next();
            }
        });

        task.AddTask(function () {
            if ($rootScope.GetChatType() === $rootScope.chatAliases.UID){
                $rootScope.$broadcast(window._messages_.dialogsList.getConfUsersList, [$rootScope.GetChatID(), function (uList) {
                    if (uList && uList.length){
                        uList.splice(mcService.findItemInArrayOfObj(uList, 0, "UIN"), 1);
                        uList.splice(mcService.findItemInArrayOfObj(uList, mcConst.UserInfo.UIN, "UIN"), 1);

                        uList.forEach(function (item, i) {
                            uList[i].id = userTreeID ++;
                        });

                        uList.sort(function (a,b){
                            return mcService.sortAB(a.DisplayName, b.DisplayName);
                        });
                    }

                    if (uList && uList.length){
                        defaultGroupList[4].data = uList;
                    } else {
                        defaultGroupList.splice(4,1);
                    }

                    task.Next();
                }]);
            } else {
                $rootScope.$broadcast(window._messages_.dialogsList.getQuickUserInfo, [$rootScope.GetChatID(), function (info) {
                    if (info){
                        if (info.UIN == 0){
                            defaultGroupList.splice(4,1);
                        } else {
                            defaultGroupList[4].data = [info];
                        }
                    }

                    task.Next();
                }]);
            }
        });

        task.AddTask(function () {
            $scope.sbrStructList.parse(defaultGroupList);

            defaultGroupList = $scope.sbrStructList.serialize();

            var lastID = defaultGroupList.length - 1;

            if (defaultGroupList[lastID].listName === CURRENTLIST && defaultGroupList[lastID].data){
                $scope.sbrStructList.selectRange(defaultGroupList[lastID].data[0].id, defaultGroupList[lastID].data[defaultGroupList[lastID].data.length - 1].id);
            }

            $scope.sbrText.focus();

            //$rootScope.SetBroadcast();
        });

        task.Run();
    };

    $scope.show = function(broadcasts){
        if (!downloadUploadManager){
            downloadUploadManager = new DownloadUploadManager($rootScope, {
                name: $scope.Name,
                upload  : {
                    enable          : true,
                    done            : function (chatType, ID, _info, Type, fType) {
                        // var info = _info.split(mcConst.CRLF);
                        var info = mcService.StringToObj(_info);
                        var file = {
                            "OriginalFileName" : mcService.extractFileName(info.FileName), // оригинальное название файла
                            "Size"             : info.Size,                            // размер файла в байтах
                            "Hash"             : info.Hash                             // SHA1 контрольная сумма
                        };

                        if (mcService.findItemInArrayOfObj(sendFiles, file.Hash, "Hash") === -1){
                            $scope.sbrFileResizer.show();
                            $scope.sbrFileList.show();
                            $scope.sbrFilesCount.show();

                            $scope.sbrFileList.add(file);

                            sendFiles.push(file);

                            updateFilesCount();
                        }
                    }
                }
            });

            $scope.uploadFile = downloadUploadManager.uploadFile;
            $scope.initDU     = downloadUploadManager.initViews;
            $scope.dwlMngr    = downloadUploadManager.getViews(mcLang(33)); // "33" :"Отмена",
        }

        view = initSendBroadcastView($scope, broadcasts);

        downloadUploadManager.canStart();
    };

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

    $scope.$on('hide' + $scope.Name, function(){
        close();
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        $scope.getData();
    });
}
function initChatFrame($scope){
    "use strict";

    var geoTimeDiapason = [
        {id: 1, value: mcLang(735)}, // "735":"сейчас",
        {id: 2, value: mcLang(736)}, // "736":"сегодня",
        {id: 3, value: mcLang(737)}, // "737":"вчера",
        {id: 4, value: mcLang(738)}, // "738":"3 дня",
        {id: 5, value: mcLang(739)}, // "739":"неделя",
        {id: 6, value: mcLang(740)}, // "740":"месяц",
    ];
    var geoClientType   = [
        {id: 1, value: mcLang(741)}, // "741":"Все устройства",
        {id: 2, value: "Android"},   //
        {id: 3, value: "iOS"},       // 
    ];

    var chatFrame = {
        id   : $scope.Name,
        rows : [
            // -- Header --
            { id: "headerWrapper" + $scope.Name, hidden: true, css: "borderBottom", 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":"Меню",
                        on: { onChange: $scope.showHideMainMenu }
                    },
                    { id: "miniUserPhoto", view: "template", template: $scope.noImage, hidden: true, borderless: true, width: 40, css: "byCenter"},
                    
                    { view: "button", id: "displayName" + $scope.Name, label: mcConst.ElisaName, 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);
                    }
                })
            },

            { id: "confCallWrapper" + $scope.Name, hidden: true, css: "borderBottom", cols: [
                {
                    view  : "button", type: "icon", id: "closeAudioConfCall", icon: "times", css: "icoButton byCenter button_raised icnBtnPd03",
                    width : 150, label : mcLang(756), // "756":"Завершить",
                    click : $scope.closeMediaConf, hidden: true
                },
                {},
                { view: "label", id: "audioConfLabel", label: mcLang(754), hidden: true, width: 180 }, // "754":"Голосовая конференция",
                {},
                {
                    view  : "button", type: "icon", id: "joinAudioConfCall", icon: "phone", css: "icoButton byCenter button_primary icnBtnPd03 noMargin",
                    width : 100, label : mcLang(755), // "755":"Войти",
                    click : $scope.joinToMediaConf, hidden: true
                }, {
                    view  : "button", type: "icon", id: "leaveAudioConfCall", icon: "phone", css: "icoButton byCenter button_danger button_raised icnBtnPd03",
                    width : 100, label : mcLang(757), // "757":"Выйти",
                    click : $scope.leaveMediaConf, hidden: true
                }
            ]},

            { height: 2 },

            // -- 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}
            ]},

            // -- 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},

            // -- GeopositionWrapper --
            { id: "geopositionWrapper", hidden: true, css: "relative whiteBG", gravity: 1, minHeight: 190, rows: [
                { borderless: true, template: '<div id="' + mcConst.googleMapsContainer + '" style="height: 100%;"></div>'},

                { height: 4},

                { cols: [
                    { id: "geoClientType", view: "richselect", options: geoClientType,   value: 1,
                        on: { onChange: $scope.refreshGeoposition }
                    },
                    { id: "geoTimeLine",   view: "richselect", options: geoTimeDiapason, value: 2,
                        on: { onChange: $scope.refreshGeoposition }
                    },
                    { view: "button", value: mcLang(734), click: $scope.refreshGeoposition}, // "734":"Обновить",
                    {},
                    { view: "button", value: mcLang(12),  click: $scope.hideGeoposition},    // "12" :"Закрыть",
                ]}
            ]},

            { view: "resizer", id: "mapsResizer", 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", hidden: isMobile},

            // -- Input Text --
            { id: "inputWrapper", height: mcConst.minEnterTextHeight, minHeight: mcConst.minEnterTextHeight, hidden: true, 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: [
                        { template: "<span class='webix_icon fa-reply fAwesome' style='margin-top: 8px; margin-left: 5px'></span>", borderless: true, width: 28},
                        { 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: "editMessageWrapper", css: "borderBottom lHeight28 _info", hidden: true, height: 30, cols: [
                        { template: "<span class='webix_icon fa-pencil fAwesome' style='margin-top: 8px; margin-left: 5px'></span>", borderless: true, width: 28},
                        { id: "editBlock", template: "", borderless: true, css: "replyBlock"},
                        { view: "button", value: "x", width: 32, css: "icoButton byCenter button_primary iconNoPadding", click: $scope.closeEditMsg },
                        { 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();

                    $scope.enterChatText.setValue('');
                    $scope.enterChatText.$setValue('');

                    setTimeout(function () {
                        $scope.enterChatText.focus();
                    }, 50);
                }}
            ], on: {
                onViewResize: $scope.saveTextInputSize
            }}
        ]
    };

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

    $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: "startVoiceConfBtn" + $scope.Name, view: "button", type: "icon", icon: "microphone", css: "icoButton", height: 27, hidden: $scope.isWeb || !$scope._rights.QAllowCreateVoiceConf,
                click: $scope.startVoiceConf, label: mcService.Lang(754)}, // "754":"Голосовая конференция",

            { id: "clearMsgHistoryBtn" + $scope.Name, view: "button", type: "icon", icon: "calendar-o", css: "icoButton", height: 27,
                click: $scope.clearHistoryContext, label: mcService.Lang(683)}, // "683":"Скрыть загруженную историю",

            { id: "getGeopositionBtn" + $scope.Name, view: "button", type: "icon", icon: "map-marker", css: "icoButton", height: 27,
                click: $scope.showGeoposition, label: mcService.Lang(733)}, // "733":"Запрос геопозиции",

            { id: "quitFromConf" + $scope.Name, view: "button", type: "icon", icon: "sign-out", css: "icoButton", height: 27, hidden: mcConst.UserInfo.hasInvite,
                click: $scope.quitFromConfContext, label: mcService.Lang(28)}, // "28" :"Выйти из конференции",

            { id: "closePrivate" + $scope.Name, view: "button", type: "icon", icon: "sign-out", css: "icoButton", height: 27, hidden: mcConst.UserInfo.hasInvite,
                click: $scope.closePrivateContext, label: mcService.Lang(552)}, // "552":"Закрыть диалог",

            { id: "closeConference" + $scope.Name, view: "button", type: "icon", icon: "sign-out", css: "icoButton", height: 27, hidden: mcConst.UserInfo.hasInvite,
                click: $scope.closeConference, label: mcService.Lang(760)} // "760":"Удалить конференцию",

        ]}
    });

    $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":"Профиль пользователя",

            { id: "adminsBtns" + $scope.Name, view: "button", type: "icon", icon: "comment-o", css: "icoButton", height: 27,
                label: mcService.Lang(746), hidden: !$scope._rights.adminFunc, // "746":"Администрирование",
                click: function () {
                    $scope.administrativeList.define('data', []
                        .concat($scope._rights.QOperatorBan ?                [{ id:"1", value:mcLang(747), icon:"ban"}] : [])   // "747":"Бан",
                        .concat($scope._rights.QOperatorTurnOutFromChannel ? [{ id:"2", value:mcLang(748), icon:"sign-out"}] : []) // "748":"Выгнать из конференции",
                        .concat($scope._rights.QKill ?      [{ id:"3", value:mcLang(749), icon:"bolt"}] : [])  // "749":"Отлючить от сервера",
                        .concat($scope._rights.QHalt ?      [{ id:"4", value:mcLang(750), icon:"bolt"}] : [])  // "750":"Завершить приложение",
                        .concat($scope._rights.QBlockUIN ?  [{ id:"5", value:mcLang(751), icon:"times"}] : []) // "751":"Заблокировать",
                        .concat($scope._rights.QFireUsers ? [{ id:"6", value:mcLang(752), icon:"gavel"}] : []) // "752":"Уволить",
                    );
                    $scope.administrativeList.show(this.getNode(), {pos: "right", y: -5});
                }
            },

        ]},

        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 }
    });

    $scope.administrativeList = webix.ui({
        view     :"submenu",
        id       :"administrativeList",
        padding  : 0,
        autowidth: true,
        type     : { height: 28 },
        on       : { onItemClick: $scope.createBAN }
    });

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

    $$($scope.container).addView(chatFrame);

    // $scope.callManager.initCallBtns();

    // -- Header --
    $scope.headerWrapper     = $$("headerWrapper" + $scope.Name);
    $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);
    $scope.getGeopositionBtn = $$("getGeopositionBtn" + $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");
    // -- Reply --
    $scope.replyBlock        = $$("replyBlock");
    $scope.replyBlockWrapper = $$("replyBlockWrapper");
    // -- Edit Message --
    $scope.editBlock         = $$("editBlock");
    $scope.editMessageWrapper= $$("editMessageWrapper");
    // -- Search --
    $scope.searchWrapper     = $$("searchWrapper"  + $scope.Name);
    $scope.searchChatText    = $$("searchChatText" + $scope.Name);
    // -- MediaCall --
    $scope.videoResizer      = $$("videoResizer");
    $scope.videoFrameWrapper = $$("videoFrameWrapper");
    // -- Geoposition Wrapper --
    $scope.mapsResizer       = $$("mapsResizer");
    $scope.geopositionWrapper= $$("geopositionWrapper");
    $scope.geoClientType     = $$("geoClientType");
    $scope.geoTimeLine       = $$("geoTimeLine");
    // ---------------
    $scope.adminsBtns        = $$("adminsBtns" + $scope.Name);
    // -- Audio Conf Btn --
    $scope.audioConfLabel     = $$("audioConfLabel");
    $scope.joinAudioConfCall  = $$("joinAudioConfCall");
    $scope.leaveAudioConfCall = $$("leaveAudioConfCall");
    $scope.closeAudioConfCall = $$("closeAudioConfCall");
    $scope.confCallWrapper    = $$("confCallWrapper" + $scope.Name);

    $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 $$($scope.Name);
}
function chatFrameController($scope, $rootScope, mcPlaySound, messageRenderer, /*mcCallManager,*/ mcVoiceRecorder, mcEmojiList, mcAudioConf){
    "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.geoIframe          = 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,
        QAllowExitTextConfs: false,
        QAllowViewUserGeoPosition: false,
        adminFunc: false,

        QAllowCreateVoiceConf: false,
        QAllowCreateVideoConf: false,
        QAllowCreateWebinar: false,
        QAllowCloseMediaConf: false,
        QAllowEnterMediaConf: false,
        QAllowLeaveMediaConf: 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 msgTemplate       = "";
    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 editMsgIdx        = 0;
    var integrations      = {
        Asterisk      : false, // включена интеграция с Asterisk
        AutoNotifiers : false  // (8.7+) автоматические "важные" уведомления (Ctrl+4)
    };
    var currentConfInfo   = {};
    var wndMsgTools       = null;
    var REPLAYPRESET      = "ReplayPreset";
    var EDITPRESET        = "EditPreset";

    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
        }
    });

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

    messageRenderer.framesList = framesList;
    messageRenderer.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;

        var selObj = window.getSelection();

        if (!selObj.toString() || selObj.focusNode.wholeText !== this.innerText) {
            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 _deleteMessage(yes, uinUID, idx, cmd) {
        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 || msgType === mcConst._CMD_.msgType.TEXT_MODIFY ? (msg.replace(/\n/gi, '\r\n')) : msg;
            }

            saveMsgTemplate("", frameID, true);

            msgTemplate = "";

            if (editMsgIdx){
                $rootScope.SendCMDToServer([
                    mcConst._CMD_.cs_try_to_change_private_message,
                    mcConst.SessionID,

                    JSON.stringify({
                        UIN: UIN || $rootScope.GetChatID(),
                        ID : editMsgIdx,
                        Msg: msg
                    })
                ]);
            } else {
                $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);
            
            msgTemplate = "";

            if (editMsgIdx){
                $rootScope.SendCMDToServer([
                    mcConst._CMD_.cs_try_to_change_conf_message,
                    mcConst.SessionID,

                    JSON.stringify({
                        UID: UID || $rootScope.GetChatID(),
                        ID : editMsgIdx,
                        Msg: msg
                    })
                ]);
            } else {
                $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.getGeopositionBtn.hide();
        
        switch ($rootScope.GetChatType()){
            case $rootScope.chatAliases.UIN:
                $scope.quitFromConf.hide();

                if ($scope._rights.QAllowViewUserGeoPosition &&
                    $rootScope.GetChatType()   === $rootScope.chatAliases.UIN &&
                    $rootScope.GetChatTypeID() !== $rootScope.chatAliases.Elisa)
                {
                    $scope.getGeopositionBtn.show();
                }
            break;

            case $rootScope.chatAliases.UID:
                $scope.quitFromConf   [$scope._rights.QAllowExitTextConfs       ? 'show' : 'hide']();
            break;
        }
        
        $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");
        }

        $scope._rights.adminFunc = false;

        /*$scope._rights.adminFunc =
            $scope._rights.QBlockUIN ||
            $scope._rights.QOperatorBan ||
            $scope._rights.QFireUsers ||
            $scope._rights.QKill ||
            $scope._rights.QHalt ||
            $scope._rights.QOperatorTurnOutFromChannel;

        if ($scope._rights.adminFunc) {
            $scope.adminsBtns.show();
        } else {
            $scope.adminsBtns.hide();
        }*/

        if (!$scope.isWeb && $scope._rights.QAllowCreateVoiceConf){
            $$("startVoiceConfBtn" + $scope.Name).show();
        } else {
            $$("startVoiceConfBtn" + $scope.Name).hide();
        }
    }

    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, confInfo){
        frameID  = $rootScope.GetChatTypeID();

        currentConfInfo = confInfo;

        if (mainView) {
            mainView.show();

            $scope.headerWrapper.show();
            $scope.inputWrapper.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;
            // editMsgIdx    = 0;

            $scope.geoIframe = null;

            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();
            $scope.geopositionWrapper.hide();
            $scope.mapsResizer.hide();

            $scope.closeReplyMsg();
            $scope.closeEditMsg();

            msgBackupIdx = msgBackupCount;

            switch ($rootScope.GetChatType()){
                case $rootScope.chatAliases.UIN:
                    messageRenderer.setMyMessages(mcService.Marge(messageRenderer.getMyMessagesTemp(), $rootScope.$broadcast(window._messages_.dialogsList.getMyMessagesState, [$rootScope.GetChatID()])));

                    $scope.topLabelList = userMenu();  // private

                    $$($scope.container).getNode().className = "webix_view noMargin gradient2 webix_layout_line";

                    !isMobile && $scope.inputResizer.show();
                    $scope.inputWrapper .show();
                    $scope.miniUserPhoto.show();

                    // $scope.quitFromConf.hide();
                    !mcConst.UserInfo.hasInvite && $scope.closePrivate.show();

                    // $rootScope.$broadcast(window._messages_.callManager.setUserInfo, [$rootScope.GetChatID(), topBtnTitle]);

                    if (frameID === $rootScope.chatAliases.Elisa){
                        $scope.miniUserPhoto.define('template', $scope.noImage);
                        $scope.miniUserPhoto.refresh();
                    }

                    // $$("startAudioConfCall").hide();
                    // $$("closeAudioConfCall").hide();
                break;

                case $rootScope.chatAliases.UID:
                    $scope.topLabelList = confMenu();  // channel

                    $$($scope.container).getNode().className = "webix_view noMargin gradient webix_layout_line";

                    !isMobile && $scope.inputResizer.show();
                    $scope.inputWrapper .show();
                    $scope.miniUserPhoto.hide();

                    // $scope.quitFromConf.show();
                    $scope.closePrivate.hide();
                break;

                default:
                    $$($scope.container).getNode().className = "webix_view noMargin webix_layout_line";
            }

            currentConfInfo && mediaConfButtons(currentConfInfo.State ? "start" : "close");

            $scope.isConf = ($rootScope.GetChatType() === $rootScope.chatAliases.UID) || $rootScope.GetChatID() == 0;

            if ($scope.isConf) {
                $rootScope.$broadcast(window._messages_.callManager.hideCallBtns);

                if (currentConfInfo && currentConfInfo.Invoker == mcConst.UserInfo.UIN) {
                    mcAudioConf.setMID(currentConfInfo.UID, true);
                }
            } else {
                $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.enterChatText.blur();
            $scope.displayName.focus();

            setTimeout(function () {
                $scope.enterChatText.focus();
                
                mcService.setCaretPosition($scope.enterChatText.getInputNode(), cursorPosition);
            }, 150);
        }
    }

    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();
        // $scope.editMessageWrapper.hide();

        if (replyToMsgIdx || editMsgIdx) {
            focusToEnterChat();
        }

        // replyToMsgIdx = 0;
        // editMsgIdx    = 0;

        $scope.closeReplyMsg();
        $scope.closeEditMsg();

        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;

            $scope.hideGeoposition();
            
            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 loadMsgTemplate() {
        function _get(data) {
            var txt = "";

            try {
                txt = $rootScope.isWebClient && !!window.__aesCTR
                    ? __aesCTR.decrypt(data, mcConst.LoginInfo.pwd, 256)
                    : data;
            } catch (e) {

            }

            return txt;
        }

        var id = mcConst.ServerInfo.ID + "_templ_" + $rootScope.GetChatTypeID() + "-";

        if (saveMsgTempList[id]) {
            var txt = _get(saveMsgTempList[id]);

            $scope.enterChatText.setValue(txt);

            focusToEnterChat(txt.length);
        } else {
            $scope.enterChatText.setValue("");

            $rootScope.Storage.msgTemplate.load(
                mcConst.ServerInfo.ID,
                mcConst.UserInfo.UIN,
                id,

                function(tempMsg) {
                    saveMsgTempList[id] = tempMsg || "";

                    if (tempMsg){
                        var txt = _get(tempMsg);

                        $scope.enterChatText.setValue(txt);

                        focusToEnterChat(txt.length);
                    }
                }
            );
        }
    }

    function saveMsgTemplate(msg, uinUID) {
        var id   = mcConst.ServerInfo.ID + "_templ_" + uinUID + "-";
        var temp = $rootScope.isWebClient && !!window.__aesCTR && msg != "" ? __aesCTR.encrypt(msg, mcConst.LoginInfo.pwd, 256) : msg;

        if (temp !== saveMsgTempList[id]){
            saveMsgTempList[id] = temp;

            $rootScope.Storage.msgTemplate.save(
                mcConst.ServerInfo.ID,
                mcConst.UserInfo.UIN,
                id,

                saveMsgTempList[id]
            );
        }
    }

    function showQuickMsgEditor(){
        function saveQuickTemplates(){
            var res = {Others: ""};

            for (var i = 1; i <= 9; i ++) {
                mcConst.UserInfo.Templates[i] = res[i] = templateForm.elements[i].getValue();
            }

            $rootScope.$broadcast('SendCMDToServer' , [
                mcConst._CMD_.cs_update_msgtemplates,
                mcConst.SessionID,

                JSON.stringify(res)
            ]);

            $scope.getPhraseList(mcConst.UserInfo.Templates);

            $scope.editMessagestemplate.close();
        }

        $scope.editMessagestemplate = webix.ui({
            view    : "window",
            position: "center",
            modal   : true,
            width   : 350,
            height  : document.body.offsetHeight < 446 ? document.body.offsetHeight : 446,
            head    : { cols:[
                { width: 42},
                { view: "label",  label:'<div class="byCenter toUpper">' + mcService.Lang(656) + "</div>"}, // "656":"Список быстрых фраз",
                { view: "button", type: 'icon', width: 42, icon: 'times', css: "button_transparent", click: function() {$scope.editMessagestemplate.close()}}
            ]},
            body    : { view: "form", padding: 0, elements: [
                { name: "1", view: "text", value: mcConst.UserInfo.Templates[1] || "", css: "noMargin", placeholder: mcLang(619) + " 1"}, // "619":"Фраза",
                { name: "2", view: "text", value: mcConst.UserInfo.Templates[2] || "", css: "noMargin", placeholder: mcLang(619) + " 2"},
                { name: "3", view: "text", value: mcConst.UserInfo.Templates[3] || "", css: "noMargin", placeholder: mcLang(619) + " 3"},
                { name: "4", view: "text", value: mcConst.UserInfo.Templates[4] || "", css: "noMargin", placeholder: mcLang(619) + " 4"},
                { name: "5", view: "text", value: mcConst.UserInfo.Templates[5] || "", css: "noMargin", placeholder: mcLang(619) + " 5"},
                { name: "6", view: "text", value: mcConst.UserInfo.Templates[6] || "", css: "noMargin", placeholder: mcLang(619) + " 6"},
                { name: "7", view: "text", value: mcConst.UserInfo.Templates[7] || "", css: "noMargin", placeholder: mcLang(619) + " 7"},
                { name: "8", view: "text", value: mcConst.UserInfo.Templates[8] || "", css: "noMargin", placeholder: mcLang(619) + " 8"},
                { name: "9", view: "text", value: mcConst.UserInfo.Templates[9] || "", css: "noMargin", placeholder: mcLang(619) + " 9"},
                { cols: [
                    {},
                    { view: "button", value: mcLang(77), click: saveQuickTemplates} // "77" :"Сохранить",
                ]}
            ]},
            on: {
                onShow: function () {
                    $rootScope.hotKeyDispatcher.addPreset("editMessagestemplate", [{
                        key   : mcConst.keyCodes.enter,
                        func  : saveQuickTemplates
                    }, {
                        key   : mcConst.keyCodes.tab,
                        shiftKey: true,
                        func  : function () { return true }
                    }, {
                        key   : mcConst.keyCodes.esc,
                        func  : function () { $scope.editMessagestemplate.close() }
                    }], document, true);
                },
                onDestruct: function () {
                    templateForm = null;
                    
                    $rootScope.hotKeyDispatcher.removePreset("editMessagestemplate");
                }
            }
        });

        var templateForm = $scope.editMessagestemplate.getChildViews()[1];

        $scope.editMessagestemplate.show();
    }

    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', titleValue[0] + (titleValue[1] ? ": " + titleValue[1] : "")); // (count ? "(" + count + ") " : "")
            $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 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 _editMessage(id){
        editMsgIdx = id;

        var msgDIV  = document.getElementById(`hsMsg-${editMsgIdx}`).cloneNode(true);
        var childList = msgDIV.querySelectorAll('*');
        var editDIV = $scope.editBlock.getNode();
        var text    = "";

        if (replyToMsgIdx){
            // $scope.replyBlockWrapper.hide();
            $scope.closeReplyMsg();
        }

        if (wndMsgTools) wndMsgTools.hide();

        $scope.editMessageWrapper.show();

        if (msgDIV.className.indexOf("msgSeparator") >= 0){
            msgDIV.className = msgDIV.className.replace("msgSeparator", "");
        }

        childList.forEach(function (item){
            if (item.className === "quoteMessage" ||
                item.className.indexOf("msgMenu") >= 0 ||
                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.className.indexOf("messageText") >= 0){
                item.childNodes.forEach(function (elm) {
                    switch (elm.nodeName){
                        case "#text":
                            text += elm.data;
                        break;
                        case "A":
                            text += elm.innerText;
                        break;
                        case "BR":
                            text += "\n";
                        break;
                    }
                });
            }

            if (item.tagName   === "IMG")      { item.style.maxHeight     = "28px"; }
            if (item.className === "emojiSize"){ item.style.verticalAlign = "middle"; }
            if (item.id)                       { item.id += "_reply_"; }
        });

        editDIV.style.position = "relative";
        editDIV.innerHTML = "";
        editDIV.appendChild(msgDIV);

        text && $scope.enterChatText.setValue(text);

        $rootScope.hotKeyDispatcher.addPreset(EDITPRESET, [{
            key   : mcConst.keyCodes.esc,
            lockPrev: true,
            func  : function () {
                $scope.closeEditMsg();
            }
        }], document);

        focusToEnterChat();
    }

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

    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();
    };

    // === Geo Position ====================
    
    window.addEventListener("message", function _get_msg(e){
        switch (e.data){
            case "geo_ready":
                $scope.refreshGeoposition();
            break;

            case "geo_anyClick":
                $scope.topContextMenu.hide();
            break;
        }
    });

    function getGeopositionData (cb){
        var clientType = mcConst._CMD_.ClientType.ALL;
        var dtUTCStart = 0;
        var dtUTCEnd   = 0;

        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_get_user_gps_position,
            mcConst.SessionID,
            $rootScope.GetChatID()
        ]);

        // sc_user_gps_history = "815D"; //(8.12+)
        switch ($scope.geoClientType.getValue()){
            case 1: clientType = mcConst._CMD_.ClientType.ALL;     break;
            case 2: clientType = mcConst._CMD_.ClientType.ANDROID; break;
            case 3: clientType = mcConst._CMD_.ClientType.IOS;     break;
        }

        switch ($scope.geoTimeLine.getValue()){
            case 1: // * 1) сейча   | dtUTCStart: now-15min  to dtUTCEnd: now
                dtUTCStart = mcService.utcTime(moment().subtract(15, 'minutes').toDate(), true);
                dtUTCEnd   = mcService.utcTime(new Date(), true);
                break;

            case 2: // * 2) сегодня | dtUTCStart: 00:00      to dtUTCEnd: now
                dtUTCStart = mcService.utcTime(moment().startOf('days').toDate(), true);
                dtUTCEnd   = mcService.utcTime(new Date(), true);
                break;

            case 3: // * 3) вчера   | dtUTCStart: 00:00 -1d  to dtUTCEnd: 00:59 -1d
                dtUTCStart = mcService.utcTime(moment().subtract(1, 'days').startOf('days').toDate(), true);
                dtUTCEnd   = mcService.utcTime(moment().subtract(1, 'days').endOf('days').toDate(), true);
                break;

            case 4: // * 4) 3 дня   | dtUTCStart: 00:00 -3d  to dtUTCEnd: now
                dtUTCStart = mcService.utcTime(moment().subtract(3, 'days').toDate(), true);
                dtUTCEnd   = mcService.utcTime(new Date(), true);
                break;

            case 5: // * 5) неделя  | dtUTCStart: 00:00 -7d  to dtUTCEnd: now
                dtUTCStart = mcService.utcTime(moment().subtract(7, 'days').toDate(), true);
                dtUTCEnd   = mcService.utcTime(new Date(), true);
                break;

            case 6: // * 6) месяц   | dtUTCStart: 00:00 -30d to dtUTCEnd: now
                dtUTCStart = mcService.utcTime(moment().subtract(1, 'months').toDate(), true);
                dtUTCEnd   = mcService.utcTime(new Date(), true);
                break;
        }

        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_get_user_gps_history,
            mcConst.SessionID,

            $rootScope.GetChatID(),// идентификатор пользователя
            dtUTCStart,            // дата и время начала диапазона в UTC формате dd.mm.yyyy.hh.nn.ss
            dtUTCEnd,              // дата и время конца диапазона в UTC формате dd.mm.yyyy.hh.nn.ss
            clientType,            // тип приложения: any, android, ios

            function (data){
                cb(data);
            }
        ]);
    }

    $scope.showGeoposition = function (){
        $scope.topContextMenu.hide();

        if ($scope._rights.QAllowViewUserGeoPosition && !$scope.geoIframe &&
            $rootScope.GetChatType()   === $rootScope.chatAliases.UIN &&
            $rootScope.GetChatTypeID() !== $rootScope.chatAliases.Elisa)
        {
            $scope.geopositionWrapper.show();
            $scope.mapsResizer.show();

            document.getElementById(mcConst.googleMapsContainer).innerHTML = '<iframe id="geoIframe" src="./viewgeo.html" style="width: 100%; height: 100%; border: none"></iframe>';

            $scope.geoIframe = document.getElementById("geoIframe");
        }
    };

    $scope.hideGeoposition = function (){
        var geo = document.getElementById(mcConst.googleMapsContainer);

        if (geo){
            geo.innerHTML = "";
            $scope.geoIframe = null;

            $scope.geopositionWrapper.hide();
            $scope.mapsResizer.hide();
        }
    };

    $scope.refreshGeoposition = function (){
        getGeopositionData(function (data) {
            // "UIN" : 17, // идентификатор пользователя
            // "Data" : [ // массив объектов
            // {
            //     "id"        : 17, // порядковый номер записи в базе данных
            //     "Latitude"  : "50.08006205562636", // широта
            //     "Longitude" : "29.931609258055687", // долгота
            //     "Error"     : 0, // 0 - всё ок, 1 - permission denied, 2 - gps disabled
            //     "AppType"   : "android", // тип приложения: ios, android
            //     "dtUTC"     : "26.11.2021.09.18.31", // дата и время геопозиции в UTC формате dd.mm.yyyy.hh.nn.ss
            // },
            // ...
            // ]

            // UIN      : 15427
            // Latitude : 50.0763343
            // AppType  : "android"
            // Longitude: 29.9122542
            // id       : 207
            // dtUTC    : "04.11.2021.06.59.26"

            var coords = [];

            data.Data.forEach(function (item){
                coords.push({
                    coords: [item.Latitude, item.Longitude],
                    title : mcService.utcTime(item.dtUTC, false, "dd.mm.yyyy hh:nn")
                });
            });

            // var msg = mcService.ObjToString(coords);

            $scope.geoIframe && $scope.geoIframe.contentWindow.postMessage({
                name  : "mychatGeoCoords",
                coords: coords
            }, '*');
        });
    };

    $scope.updateGeoposition = function (info){

    };

    // === Voice Conference ==================================

    function mediaConfButtons(type){
        // console.log("mediaConfButtons: " + type);

        if (!currentConfInfo){
            console.warn("mediaConfButtons: currentConfInfo is undefined!!");
            
            return;
        }

        switch(type){
            case "start":
                $scope.confCallWrapper.show();
                $scope.audioConfLabel.show();

                if (currentConfInfo.Invoker == mcConst.UserInfo.UIN) {
                    $scope.closeAudioConfCall.show();
                } else {
                    $scope.closeAudioConfCall.hide();
                }

                if (mcAudioConf.isLive()) {
                    $scope.joinAudioConfCall .hide();
                    $scope.leaveAudioConfCall.show();
                } else {
                    $scope.joinAudioConfCall .show();
                    $scope.leaveAudioConfCall.hide();
                }
            break;

            case "join":
                $scope.joinAudioConfCall .hide();
                $scope.leaveAudioConfCall.show();
            break;

            case "leave":
                if (currentConfInfo.State) $scope.audioConfLabel.show();

                if (currentConfInfo.Invoker == mcConst.UserInfo.UIN) {
                    $scope.closeAudioConfCall.show();
                } else {
                    $scope.closeAudioConfCall.hide();
                }

                $scope.joinAudioConfCall .show();
                $scope.leaveAudioConfCall.hide();
            break;

            case "close":
                $scope.audioConfLabel    .hide();
                $scope.closeAudioConfCall.hide();
                $scope.joinAudioConfCall .hide();
                $scope.leaveAudioConfCall.hide();
                $scope.confCallWrapper   .hide();
            break;
        }
    }

    $scope.startVoiceConf = function (){
        $scope.topContextMenu.hide();

        if (currentConfInfo && !currentConfInfo.State) { // todo: добавить проверку на права
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_create_media_conf,
                mcConst.SessionID,

                getCurrentID(),
                mcConst.mediaConf.VOICE
            ]);
        }
    };

    $scope.joinToMediaConf = function (){
        var uid = mcAudioConf.getMID() || getCurrentID();

        mcAudioConf.joinToConf(uid, function (info) {
            switch (info.Type) {
                case mcConst.mediaConf.VOICE:
                    mediaConfButtons("join");
                break;

                case mcConst.mediaConf.VIDEO:
                break;

                case mcConst.mediaConf.WEBINAR:
                break;
            }
        });
    };

    $scope.closeMediaConf = function () {
        var uid = mcAudioConf.getMID();

        if (uid) {
            mcAudioConf.closeConf(uid);
        }
    };

    $scope.leaveMediaConf = function () {
        mcAudioConf.leaveConf(function () {
            mediaConfButtons('leave');
        });
    };

    $scope.confStatusChange = function (info){
        if (currentConfInfo){
            currentConfInfo.Invoker = parseInt(info.Invoker);

            if (info.State && currentConfInfo.Invoker == mcConst.UserInfo.UIN) {
                mcAudioConf.setMID(currentConfInfo.UID, true);

                $scope.joinToMediaConf();
            } else

            if (!info.State) {
                mcAudioConf.closeCall(info.UID);
            }

            mediaConfButtons(info.State ? "start" : "close");
        }
    }

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

    $scope.scrollDown = function () {
        scrollDown(frameID, false, true);
    };

    $scope.showHideMainMenu = function(val){
        var bdg = document.getElementById("_uniBadge");

        if (!bdg && $scope.hideMainMenu){
            bdg = $scope.getUniBadge();
            $scope.hideMainMenu.getNode().appendChild(bdg);
        }

        if (val) {
            $rootScope.$broadcast(window._messages_.chatWrapper.hideLeftSide);

            if (_uniBadgeCount && $scope.hideMainMenu){
                mcService.showView(bdg);
            }

            menuIsHidden = true;
        } else {
            $rootScope.$broadcast(window._messages_.chatWrapper.showLeftSide);

            mcService.hideView(bdg);

            menuIsHidden = false;
        }
    };

    $scope.openDialogFromContext = function(){
        if (contextUser !== null) {
            $rootScope.$broadcast('OpenPrivate', [contextUser]);

            $scope.userContextMenu.hide();
        }
    };

    $scope.getUserInfoFromContext = function (){
        if (contextUser) {
            if ($scope._rights.QAllowViewRemoteUserProfile){
                $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.UserProfile, contextUser]);
                $rootScope.$broadcast("selectTool");
            }
        }

        $scope.userContextMenu.hide();
    };

    $scope.showContextUserMenu = function(){

    };

    $scope.hideContextUserMenu = function(){
        contextUser = null;
    };

    $scope.clearHistoryContext = function(){
        // this.getTopParentView().hide();
        $scope.topContextMenu.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.quitFromConfContext = function () {
        // this.getTopParentView().hide();
        $scope.topContextMenu.hide();
        
        $rootScope.$broadcast('leaveConf', [$rootScope.GetChatID()]);
    };

    $scope.closePrivateContext = function () {
        // this.getTopParentView().hide();
        $scope.topContextMenu.hide();

        $rootScope.$broadcast('removeDialogFromList', [$rootScope.GetChatID()]);
    };

    $scope.sendQuickMsg = function(id){
        var msg = $scope.visualMsgTempList.getItem(id);

        $scope.clipPopupWindow.hide();

        if (msg.id === 9999) {
            showQuickMsgEditor();
        } else
        if (msg && msg.value) {
            var textArea = $$("enterChatText"  + $scope.Name).getInputNode();

            cursorPosition = mcService.insertAtCursor(textArea, msg.value);

            focusToEnterChat();

            if (!$rootScope.isWebClient) $rootScope.$broadcast(window._messages_.mcStatistics.setStatValue, ["n_srv_fastmsg", 1, false]); // false - увеличить значение на "1"
        }
    };

    $scope.getPhraseList = function(list){
        msgTemplateList = [];

        if (list){
            Object.keys(list).forEach(function (id) {
                if (id !== "Others") {
                    msgTemplateList.push({
                        id   : parseInt(id),
                        value: list[id].replace(/</g,"&lt;")
                    });
                }
            });
        }

        msgTemplateList.push({
            id   : 9999,
            value: mcService.insertIco("fa-pencil-square-o", "lHeight26 font110") + mcLang(655) // "655":"Редактировать список фраз",
        });

        if ($scope.visualMsgTempList) {
            $scope.visualMsgTempList.define("data", msgTemplateList);
            $scope.visualMsgTempList.refresh();
            $scope.visualMsgTempList.sort("id", "asc");
        }

        return msgTemplateList;
    };

    $scope.drawSmiles = function(){
        var res = [];
        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);
        },

        "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);
            }
        },

        "playMediaFile"      : function openInFolder (ev){  // todo: remove
            var element  = ev.target;
            var parent   = element.parentElement.parentElement;
            var video    = document.createElement("video");

            video.src = element.getAttribute("link");
            video.style.maxWidth  = "100%";
            video.style.maxHeight = "400px";
            video.style.display   = "block";
            video.controls        = "controls";
            video.autoplay        = true;

            parent.appendChild(video);

            mcService.hideView(element);

            setTimeout(function (){
                video.focus();

                focusToEnterChat();
            }, 50);
        },

        "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":"Текст скопирован в буфер обмена",
            }
        },

        "msgMenu"            : function (ev) {
            var menu      = ev.target;
            var Idx       = parseInt(menu.getAttribute("Idx"));
            var msgType   = parseInt(menu.getAttribute("type"));
            var uin       = parseInt(menu.getAttribute("uin"));
            var dt        = menu.getAttribute("time").split(',');
            var replyHint = mcLang(725); // "725":"Ответить";
            var delHint   = mcLang(519); // "519":"Удалить";
            var editHint  = mcLang(761); // "761":"Редактировать сообщение",
            var isConf    = $rootScope.GetChatType() === $rootScope.chatAliases.UID;
            var canDelConfMsg  = mcService.checkRights(mcConst._CMD_.RS.QAllowRemoveConfMessages);
            var canDelPrivMsg  = mcService.checkRights(mcConst._CMD_.RS.QAllowRemovePrivateMessages);
            var canEditConfMsg = mcService.checkRights(mcConst._CMD_.RS.QAllowEditSelfConfMessages);
            var canEditPrivMsg = mcService.checkRights(mcConst._CMD_.RS.QAllowEditSelfPrivateMessages);
            var delta     = dt.length === 7 ? moment(new Date()).diff(moment(new Date(dt[2], parseInt(dt[1]) - 1, dt[0], dt[3], dt[4], dt[5])), 'minutes') : 9999;

            if (wndMsgTools) {
                wndMsgTools.hide();
            }

            wndMsgTools = webix.ui({
                view  : "popup",
                width : 70,
                height: 15,
                padding: 0,
                body  : {
                    css: "byCenterTools",
                    padding: 0,
                    borderless: true,
                    onClick: $scope.msgToolsAction,
                    template: `
                        <span class='webix_icon fa-reply fAwesome replyMessage'  Idx='${Idx}' title='${replyHint}'></span>&nbsp;
                        
                        ${(isConf && canDelConfMsg)  || (!isConf && canDelPrivMsg) 
                            ? `<span class='webix_icon fa-trash-o fAwesome deleteMessage' Idx='${Idx}' title='${delHint}'></span>&nbsp;` : ``}
                        
                        ${((isConf && canEditConfMsg) || (!isConf && canEditPrivMsg)) && msgType === mcConst._CMD_.msgType.TEXT && uin == mcConst.UserInfo.UIN && delta <= mcConst.UserInfo.MaxSelfMsgEditTime  
                            ? `<span class='webix_icon fa-pencil  fAwesome editMessage'   Idx='${Idx}' title='${editHint}'></span>` : ``}
                        
                    `
                },
                on: { onClose: function () {
                    wndMsgTools = null;
                }}
            });

            wndMsgTools.show(menu, {pos: "bottom", x: -10, y: 6});
        }
    };

    $scope.msgToolsAction = {
        "deleteMessage"      : function (ev){
            var trash  = ev.target;
            var idx    = trash.getAttribute('Idx');
            var uinUID = getCurrentID();
            var elm    = "";
            var param  = "";
            var parent = document.getElementById(`hsMsg-${idx}`);

            if (wndMsgTools) wndMsgTools.hide();

            ev.preventDefault();

            if (uinUID && getCurrentType() === $rootScope.chatAliases.UID){
                elm   = parent.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);
                    }
                });
            } else
            if (uinUID && getCurrentType() === $rootScope.chatAliases.UIN) {
                elm   = parent.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);
                    }
                });
            }
        },

        "replyMessage"       : function (ev){
            replyToMsgIdx = ev.target.getAttribute('Idx');
            var msgDIV    = document.getElementById(`hsMsg-${replyToMsgIdx}`).cloneNode(true);
            var childList = msgDIV.querySelectorAll('*');
            var replyDIV  = $scope.replyBlock.getNode();

            if (wndMsgTools) wndMsgTools.hide();

            if (editMsgIdx){
                $scope.editMessageWrapper.hide();
            }

            $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("msgMenu") >= 0 ||
                    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_"; }
            });

            replyDIV.style.position = "relative";
            replyDIV.innerHTML = "";
            replyDIV.appendChild(msgDIV);

            $rootScope.hotKeyDispatcher.addPreset(REPLAYPRESET, [{
                key   : mcConst.keyCodes.esc,
                lockPrev: true,
                func  : function () {
                    $scope.closeReplyMsg();
                }
            }], document);

            focusToEnterChat();
        },
        
        "editMessage"        : function (ev) {
            _editMessage(ev.target.getAttribute('Idx'));
        }
    };

    $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();
        $rootScope.hotKeyDispatcher.removePreset(REPLAYPRESET);

        focusToEnterChat();
    };

    $scope.closeEditMsg = function (){
        editMsgIdx = 0;
        $scope.editMessageWrapper.hide();
        $rootScope.hotKeyDispatcher.removePreset(EDITPRESET);
        $scope.enterChatText.setValue("");

        focusToEnterChat();
    };

    // === 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.aup,
            func  : function (){
                var val = mcService.trim($scope.enterChatText.getValue());
                var msg;
                var showMsg = function (msg){
                    if (msg){
                        var dt    = (msg.dt || msg.dtUTC).split('.');
                        var delta = dt.length === 7 ? moment(new Date()).diff(moment(new Date(dt[2], parseInt(dt[1]) - 1, dt[0], dt[3], dt[4], dt[5])), 'minutes') : 9999;

                        if (delta <= mcConst.UserInfo.MaxSelfMsgEditTime) {
                            _editMessage(msg.Idx);

                            // res = false;
                        }
                    }
                }

                if (val === ""){
                    if ($rootScope.GetChatType() === $rootScope.chatAliases.UIN) {
                        msg = $rootScope.$broadcast(window._messages_.dialogsList.getMyPrevMessagePriv, [$rootScope.GetChatID()]);

                    } else
                    if ($rootScope.GetChatType() === $rootScope.chatAliases.UID) {
                        msg = $rootScope.$broadcast(window._messages_.dialogsList.getMyPrevMessageConf, [$rootScope.GetChatID()]);
                    }

                    msg && showMsg(msg);
                }

                return true;
            }
        }, {
            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);
                }
            }
        }, {
            key     : mcConst.keyCodes.gravis,
            ctrlKey : true,
            func    : function () {
                $rootScope.$broadcast(window._messages_.chatWrapper.showHideMenu, [true]);
                $rootScope.$broadcast(window._messages_.mainMenu.selectMenuElement, [mcConst.dataModels.Terminal]);
            }
        }], 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(){
        $scope.isWeb = $rootScope.isWebClient;

        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($rootScope.chatAliases.Elisa);

            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.container).getNode().className = "webix_view noMargin gradient2 webix_layout_line";
            break;

            case $rootScope.chatAliases.UID:
                $$($scope.container).getNode().className = "webix_view noMargin gradient webix_layout_line";
            break;

            default:
                $$($scope.container).getNode().className = "webix_view noMargin webix_layout_line";
        }

        if (frameID) {
            framesList.get(frameID, noCreate).show();
        }

        downloadUploadManager.canStart();

        if (mcConst.UserInfo.hasInvite && window.isMobile) {
            $scope.showHideMainMenu(true);
            $scope.hideMainMenu.hide();

        }
    };

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

    $scope.createBAN = function (id){
        var item = $scope.administrativeList.getItem(id);

        if ($scope._rights.adminFunc) {
            switch (item.id){
                case '1':
                    
                break;

                case '2':

                break;

                case '3':

                break;

                case '4':

                break;

                case '5':

                break;

                case '6':

                break;
            }
        }
    };

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

    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',
        mark_private_got_notify : 'mark_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',
        on_sc_integrations      : 'on_sc_integrations',
        showUserGeoposition     : 'showUserGeoposition',
        on_geopositionHistory   : 'on_geopositionHistory',
        conf_status_change      : 'conf_status_change',
        media_conf_leave        : 'media_conf_leave',
        showHideMainMenu        : 'showHideMainMenu',

        '----':'----'
    };

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

    $scope.$on(_msg.showHideMainMenu, function (e, args){
        $scope.showHideMainMenu.apply(null, args);
    });

    $scope.$on(_msg.conf_status_change, function (e, args){
        $scope.confStatusChange.apply(null, args);
    });

    $scope.$on(_msg.media_conf_leave, function (e, args){
        $scope.confStatusChange.apply(null, args);
    });

    $scope.$on(_msg.on_geopositionHistory, function (e, args){
        $scope.updateGeoposition.apply(null, args);
    });

    $scope.$on(_msg.showUserGeoposition, function (e, args){
        $scope.showGeoposition();
    });

    $scope.$on(_msg.on_sc_integrations, function (e, args){
        integrations = args[0];
    });

    $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 = messageRenderer.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]);
                }
            }

            messageRenderer.setMyMessages(myMessages);
        }
    });

    $scope.$on(_msg.mark_private_got_notify, function (e, args) {
        markGotNotify.apply(null, [args[0], args[1]]);
    });

    $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) {
        messageRenderer.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();
            }

            messageRenderer.chatMessage(msg, currentFrame, $scope.chatTextOutputW.getNode());

            allBackupMsgSended(msg)
        } else {
            console.error('$scope.chatTextOutputW not found');
        }
    });

    $scope.$on(_msg.addCustomMSG, function(e, args){
        messageRenderer.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();

            messageRenderer.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();

            messageRenderer.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();
        $scope.closeReplyMsg();
        $scope.closeEditMsg();

        $scope.geopositionWrapper.hide();
        $scope.mapsResizer.hide();

        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(){
        if (mainView && mainView.isVisible()){
            var inFocus = webix.UIManager.getFocus();

            if (inFocus && inFocus.config.id === $scope.enterChatText.config.id){
                $scope.enterChatText.focus();
            }

            
        }
    }]);

    $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);
        $scope._rights.QAllowExitTextConfs         = mcService.checkRights(mcConst._CMD_.RS.QAllowExitTextConfs);
        $scope._rights.QAllowViewUserGeoPosition   = mcService.checkRights(mcConst._CMD_.RS.QAllowViewUserGeoPosition);

        $scope._rights.QBlockUIN    = mcService.checkRights(mcConst._CMD_.RS.QBlockUIN);
        $scope._rights.QOperatorBan = mcService.checkRights(mcConst._CMD_.RS.QOperatorBan);
        $scope._rights.QFireUsers   = mcService.checkRights(mcConst._CMD_.RS.QFireUsers);
        $scope._rights.QKill        = mcService.checkRights(mcConst._CMD_.RS.QKill);
        $scope._rights.QHalt        = mcService.checkRights(mcConst._CMD_.RS.QHalt);
        $scope._rights.QOperatorTurnOutFromChannel = mcService.checkRights(mcConst._CMD_.RS.QOperatorTurnOutFromChannel);

        $scope._rights.QAllowCreateVoiceConf = mcService.checkRights(mcConst._CMD_.RS.QAllowCreateVoiceConf);
        $scope._rights.QAllowCreateVideoConf = mcService.checkRights(mcConst._CMD_.RS.QAllowCreateVideoConf);
        $scope._rights.QAllowCreateWebinar   = mcService.checkRights(mcConst._CMD_.RS.QAllowCreateWebinar);
        $scope._rights.QAllowCloseMediaConf  = mcService.checkRights(mcConst._CMD_.RS.QAllowCloseMediaConf);
        $scope._rights.QAllowEnterMediaConf  = mcService.checkRights(mcConst._CMD_.RS.QAllowEnterMediaConf);
        $scope._rights.QAllowLeaveMediaConf  = mcService.checkRights(mcConst._CMD_.RS.QAllowLeaveMediaConf);

        attachButtons();
    });
}

function initBbsFrame($scope){
    "use strict";

    var view = $$($scope.Name);

    function numberList(length) {
        return (new Array(length || 1)).join('.').split('.').map(function (itm, idx) { return idx.toString() })
    }

    if (!view){
        $$($scope.container).addView({
            id   : $scope.Name,
            css  : "whiteBG",
            rows : [
                { view: "button", type: "icon", icon: "chevron-down", id: "displayName" + $scope.Name, label: mcLang(556), // "556":"Доска объявлений",
                    click: $scope.titleMenu, css: "whiteBG imgBtn button_info frameTopBtnCenter",
                    on: { onItemClick: function () {
                        if ($scope._rights.QBBSPost){
                            var btn = this.getNode();

                            setTimeout(function () {
                                $scope.clipPopupWindow.show(btn, {pos: 'bottom', x: 5, y: 10});
                            }, 100);
                        }
                    }}
                },

                { id: "bbsNoMessages" + $scope.Name, css: "whiteBG", hidden: true, rows: [
                    {},
                    { cols: [
                        {},
                        { view: "label", label: mcLang(657), css: "byCenter"}, // "657":"Нет активных объявлений",
                        {}
                    ]},
                    { cols: [
                        {},
                        { id: "btnAddBbs" + $scope.Name, view: "button", type: "icon", icon: "plus", css: "noUpperCase byCenter", label: mcLang(557), hidden: !$scope._rights.QBBSPost, click: $scope.addNewBBS }, // "557":"Новое объявление",
                        {}
                    ]},
                    {}
                ]},

                { gravity: 0 },

                { id: "bbsContent" + $scope.Name, view: "template", borderless: true, hidden: true, css: "selectable " + (!isMobile ? "scrollMenu" : ""),
                    scroll: false, template: '&nbsp;', gravity: 3},

                // { id: "bbsResizer" + $scope.Name, view: "resizer", hidden: true},

                { id: "inputWrapper" + $scope.Name, gravity: 2, hidden: true, css: "whiteBG", rows: [

                    { view: "toolbar", height: 32, css: "borderTop", padding: 0, cols: [
                        { view:"datepicker",
                            id           : "bbsLifeTime" + $scope.Name,
                            value        : new Date(),
                            labelPosition: 'left',
                            labelAlign   : 'right',
                            label        : mcLang(559) + ":", // "559":"Актуально до",
                            labelWidth   : 110,
                            width        : 240,
                            suggest      : {
                                type : "calendar",
                                body : { minDate: new Date((new Date()).getTime() - mcConst.oneDay), maxDate: new Date("3000.01.01") }
                            }
                        },

                        { id: "bbsHours" + $scope.Name, view: "richselect", width: 55, value: 23, borderless: true,
                            suggest: { fitMaster: false, width: 65,
                                data: numberList(24)
                            }
                        },

                        { view: "label", label: ":", align: "center", width: 1, padding: 0},

                        { id: "bbsMinutes" + $scope.Name, view: "richselect", width: 55, value: 59, borderless: true,
                            suggest: { fitMaster: false, width: 65,
                                data: numberList(60)
                            }
                        },

                        { id: "bbsToTop" + $scope.Name, view: "checkbox", value: false, labelWidth: 0, labelRight: mcLang(560),  autowidth: true, css: "finger"}, // "560":"Поднять вверх",

                        { id: "btnSendText" + $scope.Name, view: "button", type: "icon", icon: "send", css: "noBG noBorderAll iconNoPadding byCenter", tooltip: mcLang(558), // "558":"Опубликовать",
                            click: $scope.sendBBS, minWidth: 35, label: mcLang(558) // "558":"Опубликовать",
                        },

                        { width: 5},

                        { view: "button", type: "icon", icon: "close", css: "noBG noBorderAll byCenter button_danger iconNoPadding", tooltip: mcLang(12), // "12" :"Закрыть",
                            click: $scope.closeBBS, width: 35
                        },

                        { width: 5}
                    ]},

                    { id: "enterBBSText" + $scope.Name, view: "textarea", css: "noBGColorAll borderTop", borderless: true, placeholder: mcLang(27)}, // "27" : "Введите ваше сообщение здесь",

                ]}
            ]
        });

        $scope.clipPopupWindow = webix.ui({
            view : "popup",
            id   : "topMenu" + $scope.Name,
            head : false,
            css  : "noUpperCase",
            width: 200,
            padding: 0,
            body : {  rows: [{
                view : "button",
                type : "icon",
                icon : "plus",
                css  : "icoButton",
                label: mcLang(557), // "557":"Новое объявление",
                click: function(){
                    this.getTopParentView().hide();

                    $scope.addNewBBS();
                }
            }]}
        });

        view = $$($scope.Name);

        $scope.bbsContent   = $$("bbsContent" + $scope.Name);
        $scope.enterBBSText = $$("enterBBSText" + $scope.Name);
        $scope.bbsNoMessages= $$("bbsNoMessages" + $scope.Name);
        $scope.btnAddBbs    = $$("btnAddBbs" + $scope.Name);

        // $scope.bbsResizer   = $$("bbsResizer" + $scope.Name);
        $scope.inputWrapper = $$("inputWrapper" + $scope.Name);
        $scope.btnSendText  = $$("btnSendText" + $scope.Name);

        $scope.bbsToTop     = $$("bbsToTop" + $scope.Name);
        $scope.bbsLifeTime  = $$("bbsLifeTime" + $scope.Name);
        $scope.bbsHours     = $$("bbsHours" + $scope.Name);
        $scope.bbsMinutes   = $$("bbsMinutes" + $scope.Name);
    } else {
        view.show();

        $scope.bbsNoMessages.hide();
    }

    return view;
}
function bbsController($scope, $rootScope, mcPlaySound){
    "use strict";

    $scope.Name  = mcConst.dataModels.BBS;

    var view     = null;
    var pinned   = [];
    var ordinary = [];
    var firstBBS = null;//listBBS[0];

    $scope._rights = {
        QBBSPost: false
    };
    
    // =====================================

    function addMSG (bbs){
        // [{
        //     DisplayName: "Andrey Rakov",
        //     ID: 47,
        //     Msg: "sdfsdfsdfsdf",
        //     Top: false,
        //     UIN: 3,
        //     dtActualTo: "30.06.2017.23.59.00",
        //     dtCreated: "22.06.2017.17.11.16"
        // }]

        var div    = document.createElement('div');
        var text   = bbs.Msg.replace(/\u041e\u00ac/g, "");
        var uin    = bbs.UIN;
        var nick   = bbs.DisplayName;
        var time   = (bbs.dtCreated) ? mcService.utcTime(bbs.dtCreated).split(".") : "";
        var date   = '';
        var target = $scope.bbsContent.getNode().firstChild;

        date = (time.length === 7) ? time[0] + "." + time[1] + "." + time[2] + "&nbsp;" : time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +
        time = (time.length === 7) ? time[3] + ":" + time[4] + ":" + time[5] + "&nbsp;" : time; // dt[0] + "." + dt[1] + "." + dt[2] + "&nbsp;" +

        text = mcService.ReplaceURLs(text, { 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;
        }}).replace(/\r\n/g, "<br>");

        div.className = bbs.Top ? "messageUser" : "messageMy";

        div.innerHTML = mcService.myReplaceFormated(
            "<div id='#{id}' class='bbsMessage'>" +
                "<div><span class='bbsTime brown'>#{time}</span> <span class='bbsDate gray'>#{date}</span> <span class='bbsName linkColor finger' uin='#{uin}' onclick='_userActions.openUserPrivate.apply(this)'>#{name}</span></div><br>" +
                "<div class='bbsText'>#{text}</div>" +
            "</div>",
            {
                id  : bbs.ID,
                time: time,
                date: date,
                name: nick,
                text: text,
                uin : uin
            }
        );
        
        div.setAttribute('uin', uin);
        div.setAttribute('Idx', bbs.ID);
        div.style.wordBreak = "break-word";

        target.insertBefore(document.createElement('br'), target.firstChild);
        target.insertBefore(div, target.firstChild);
    }

    function showFileLoadingImg(){
        $scope.clipPopupButton.hide();
        $scope.clipPopupLoaderImg.show();

        $scope.enterBBSText.disable();
    }

    function hideFileLoadingImg(){
        if (!$scope.clipPopupButton.isVisible()){
            $scope.clipPopupLoaderImg.hide();
            $scope.clipPopupButton.show();

            $scope.enterBBSText.enable();
        }
    }

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

    $scope.registerHotKeys = function () {
        $rootScope.hotKeyDispatcher.addPreset(mcConst.dataModels.BBS, [{
            key   : mcConst.keyCodes.enter,
            func  : $scope.sendText
        }, {
            key   : mcConst.keyCodes.esc,
            func  : $scope.closeBBS
        }], $scope.enterBBSText.getNode());

        $rootScope.hotKeyDispatcher.addPreset(mcConst.dataModels.BBS + "global", [{
            key   : mcConst.keyCodes.n,
            altKey: true,
            func  : $scope.addNewBBS
        }], document);
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset(mcConst.dataModels.BBS);
        $rootScope.hotKeyDispatcher.removePreset(mcConst.dataModels.BBS + "global");
    };

    $scope.addNewBBS = function () {
        if ($scope._rights.QBBSPost) {
            $scope.bbsNoMessages.hide();
            
            $scope.inputWrapper.show();
            // $scope.bbsResizer.show();

            $scope.enterBBSText.focus();
        } else {
            $rootScope.$broadcast('ErrorMsg', [5, mcService.Error(5)]);
            // webix.alert(mcService.Error(5));
        }
    };

    $scope.closeBBS = function () {
        $scope.inputWrapper.hide();
        // $scope.bbsResizer.hide();

        setTimeout(function(){
            $scope.enterBBSText.setValue('');
            $scope.enterBBSText.$setValue('');
            $scope.enterBBSText.focus();

            if (!firstBBS) {
                $scope.bbsNoMessages.show();
            }
        }, 10);
    };

    $scope.sendBBS = function () {
        var msg    = mcService.trim($scope.enterBBSText.getValue());
        var expire = mcService.utcTime(mcService.formatDate($scope.bbsLifeTime.getValue(), "dd.mm.yyyy.") + $scope.bbsHours.getValue() + "." + $scope.bbsMinutes.getValue() + ".00", true);
        var sticky = mcService.convertIntToBool($scope.bbsToTop.getValue());

        if ($rootScope.isWebClient || mcConst.ClientSettings.SoundsSndChatRet) {
            mcPlaySound.PlaySound(mcPlaySound.Sounds.EnterBtn);
        }

        if (msg !== ''){
            msg = msg.replace(/\n/g, '\r\n');

            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_add_new_bbs,
                mcConst.SessionID,
                JSON.stringify({
                    Expired: expire, // 12.01.2013.14.15.00 до какого времени объявление будет актуально
                    Sticky : sticky, // Sticky "прилепленное" объявление (в самом верху) или обычное
                    Msg    : msg     // содержимое объявления
                })
            ]);

            setTimeout($scope.closeBBS, 50);
        }

        return false;
    };

    $scope.show = function(){
        view = initBbsFrame($scope);
    };
    
    $scope.getData = function (showNotify) {
        $rootScope.SendCMDToServer([
            mcConst._CMD_.cs_get_bbs,
            mcConst.SessionID,

            function (listBBS) {
                $scope.bbsContent.getNode().firstChild.innerHTML = "&nbsp;";

                pinned   = [];
                ordinary = [];

                if (listBBS && listBBS.length > 0){
                    firstBBS = listBBS[0];

                    // if (showNotify && firstBBS){
                    //     var text = firstBBS.Msg.replace(/\u041e\u00ac/g, "");
                    //     var textForNotify = text.slice(0, 100) + ((text.length > 100) ? '...' : "" );

                        // $rootScope.$broadcast('sendCMDToElectron', [
                        //     mcConst._CMD_.ce_show_notify,
                        //
                        //     firstBBS.DisplayName.slice(0, 30) + ((firstBBS.DisplayName.length > 30) ? '..' : "" ) + ":",
                        //     textForNotify,
                        //     firstBBS.ID
                        // ]);
                    // }

                    $scope.bbsNoMessages.hide();
                    $scope.bbsContent.show();

                    listBBS.forEach(function (bbs) {
                        if (bbs.Top) {
                            pinned.push(bbs);
                        } else {
                            ordinary.push(bbs);
                        }
                    });

                    pinned.sort(function (a, b) {
                        return mcService.sortAB(a.dtCreated, b.dtCreated);
                    });

                    ordinary.sort(function (a, b) {
                        return mcService.sortAB(a.dtCreated, b.dtCreated);
                    });

                    ordinary.forEach(function (bbs) {
                        addMSG(bbs);
                    });
                    
                    pinned.forEach(function (bbs) {
                        addMSG(bbs);
                    });
                } else {
                    firstBBS = null;

                    $scope.bbsContent.hide();
                    $scope.bbsNoMessages.show();
                }
            }
        ]);
    };

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

    var _msg = window._messages_.BBS = {
        refreshBBS  : 'refreshBBS'
    };

    $scope.$on(_msg.refreshBBS, function (e, args) {
        var showNotify = args[0];

        if (view){
            $scope.getData(showNotify);
        }
    });

    $scope.$on('hide' + $scope.Name, function(){
        if (view && view.isVisible()) {
            $scope.removeHotKeys();

            view.hide();
        }
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        $scope.show();
        $scope.getData();
        $scope.registerHotKeys();
    });

    $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
        $scope._rights.QBBSPost = mcService.checkRights(mcConst._CMD_.RS.QBBSPost);

        $scope.btnAddBbs && $scope.btnAddBbs[$scope._rights.QBBSPost ? "show" : "hide"]();
    });
}
function initKanbanFrame($scope, initUrl){
    "use strict";

    var view = $$($scope.Name);

    if (!view){
        $$($scope.container).addView({
            id   : $scope.Name,
            view : "iframe",
            css  : "iFrameFit",
            src  : initUrl,
            on   : {
                // onAfterLoad: $scope.onLoadKanban
            }
        });

        view = $$($scope.Name);
        
        view.showFrame = function () {
            view.define('width', undefined);
            view.resize();

            mcService.showFrame($scope.Name);
        };

        view.hideFrame = function () {
            view.define('width', '0');
            view.resize();

            mcService.hideFrame($scope.Name);
        };
    } else {
        view.showFrame();
    }

    return view;
}
function kanbanController($scope, $rootScope, mcPlaySound){
    "use strict";

    $scope.Name = mcConst.dataModels.Kanban;

    var iFrame     = null;
    var goUrl      = null;
    var closeTimer = null;
    var closeAfter = 50000;
    var viewContainer = null;

    $scope.token     = null;
    // $scope.kanbanURL = "";

    function setKanbanUrl(url) {
        if (!$scope.token) {
            goUrl = url.indexOf($scope.token) > 0 ? url : (url + $scope.getToken(url, mcConst.Lang));

            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_create_token,
                mcConst.SessionID,

                $rootScope.chatAliases.Kanban.toLowerCase(),
                goUrl,
                // $scope.kanbanURL,

                function (data) {
                    $scope.token = data.Token;

                    // $scope.show();

                    // if (!iFrame.config.src) {
                        // $scope.kanbanURL = url || $scope.kanbanURL; //(mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasKanban + "/");
                        // $scope.kanbanURL = mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasKanban + "/";

                        iFrame.define("src", goUrl + $scope.getToken(goUrl, mcConst.Lang));
                    // }
                }
            ]);
        } else {
            goUrl = url;

            if (goUrl === mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasKanban + "/"){

            } else
            if (iFrame && url.indexOf(mcService.getLocalHostPath($rootScope.isWebClient) === 0)){
                iFrame.define("src", goUrl);

                // goUrl = null;
            } else {
                console.warn("Wrong URL for iFrame:" + goUrl);
            }
        }

        // if (!$scope.token){
        //     $rootScope.SendCMDToServer([
        //         mcConst._CMD_.cs_create_token,
        //         mcConst.SessionID,
        //
        //         $rootScope.chatAliases.Kanban.toLowerCase(),
        //         $scope.kanbanURL,
        //
        //         function (data) {
        //             $scope.token     = data.Token;
        //
        //             $scope.kanbanURL = url || $scope.kanbanURL; //(mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasKanban + "/");
        //
        //             if (!iFrame.config.src) {
        //                 iFrame.define("src", $scope.kanbanURL + $scope.getToken($scope.kanbanURL, mcConst.Lang));
        //             }
        //         }
        //     ]);
        // } else {
        // }
    }

    function closeKanban(){
        clearTimeout(closeTimer);

        closeTimer = setTimeout(function () {
            $scope.token     = null;
            // $scope.kanbanURL = "";

            viewContainer && iFrame && viewContainer.removeView($scope.Name);

            iFrame = null;
        }, closeAfter);
    }

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

    $scope.getToken = function (url, lang) {
        var srch = url.indexOf("?") === -1 && url.indexOf("#") === -1 ? "?" : "&";

        return $scope.token
            ? srch +
            "token=" + $scope.token +
            (lang ? "&lang=" + lang : "")
            : "";
    };

    $scope.registerHotKeys = function () {
        // $rootScope.hotKeyDispatcher.addPreset(mcConst.dataModels.Kanban, [{
        //     key   : mcConst.keyCodes.enter,
        //     func  : function () {
        //     }
        // }], document);
    };

    $scope.removeHotKeys = function () {
        // $rootScope.hotKeyDispatcher.removePreset(mcConst.dataModels.Kanban);
    };

    $scope.show = function(){
        var initUrl = mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasKanban + "/";

        iFrame = initKanbanFrame($scope, initUrl);

        viewContainer = $$($scope.container);

        clearTimeout(closeTimer);
        closeTimer = null;
    };

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

    var _msg = window._messages_.kanban = {
        openKanbanUrl  : 'openKanbanUrl',
        closeKanban    : 'closeKanban'
    };

    $scope.$on(_msg.openKanbanUrl, function (e, args) {
        setKanbanUrl.apply(null, args);
    });

    $scope.$on(_msg.closeKanban, function () {
        if (iFrame){
            $scope.token = null;
            // $scope.kanbanURL = null;

            iFrame.define('src', "");
        }
    });

    $scope.$on('hide' + $scope.Name, function(){
        $scope.removeHotKeys();

        if (iFrame && iFrame.isVisible()) {
            iFrame.hideFrame();
        }

        closeKanban();
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        // setKanbanUrl();

        // if (!$scope.token) {
        //     $rootScope.SendCMDToServer([
        //         mcConst._CMD_.cs_create_token,
        //         mcConst.SessionID,
        //
        //         $rootScope.chatAliases.Kanban.toLowerCase(),
        //         $scope.kanbanURL,
        //
        //         function (data) {
        //             $scope.token = data.Token;
        //
        //             $scope.show();
        //
        //             if (!iFrame.config.src) {
        //                 $scope.kanbanURL = mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasKanban + "/";
        //
        //                 iFrame.define("src", $scope.kanbanURL + $scope.getToken($scope.kanbanURL, mcConst.Lang));
        //             }
        //         }
        //     ]);
        // }

        $scope.show();
    });
}
function initUserProfile($scope){
    "use strict";

    function __textInput(id, pl, lb, change, disabled, hidden){
        return {
            view         : "text",
            id           : 'up' + id,
            // value        : $scope.data[id],
            labelPosition: 'left',
            labelWidth   : labelWidth,
            height       : 33,
            css          : "noBGAll",
            placeholder  : disabled ? "" : (mcLang(pl) || pl),
            label        : mcLang(lb) || lb,
            on           : { onTimedKeyPress: change  || function(){}},
            disabled     : disabled,
            hidden       : (hidden != undefined)? hidden : false
        }
    }

    var viewContainer = $$($scope.container);
    var _view         = $$($scope.Name);
    var textHeight    = 28;
    var labelWidth    = 250;
    var labelHeight   = 37;

    if (_view && viewContainer){
        viewContainer.removeView($scope.Name);
        
        _view.destructor();
    }

    var width  = $scope.wndSize.width  >= 800 ? 600 : $scope.wndSize.width;
    var height = $scope.wndSize.height >= 800 ? 800 : $scope.wndSize.height;
    var labelMainW = 100;

    var _sex = [
        {id: '0', value: mcService.Lang(74)}, // "74" : "не определен",
        {id: '1', value: mcService.Lang(75)}, // "75" : "мужской",
        {id: '2', value: mcService.Lang(76)}  // "76" : "женский",
    ];

    var infoMainFoto = { width: 180, rows: [
        { height: 3},
        { id: 'upFoto', template: $scope.noImage, height: 170, borderless: true, css: "byCenter"}
    ]};

    var _fotoBtns = { id: "fotoEditBtns"};

    _fotoBtns[width < 600 ? "cols" : "rows"] = [
        { view:"uploader",label: mcService.Lang(78), css: '', id: 'upLoadFoto',  autosend: false, upload: "", height: textHeight, multiple: false, // "78" : "Загрузить фото",
            accept: ".jpg,.png,.jpeg", on : { onBeforeFileAdd: $scope.uploadFoto} },
        { view:"button",  label: mcService.Lang(79), css: 'button_danger', id: 'upClearFoto', height: textHeight, click: $scope.clearFoto}  // "79" : "Очистить фото",
    ];

    infoMainFoto.rows.push(_fotoBtns);

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

    $$($scope.container).addView({
        id   : $scope.Name,
        css  : "color54",
        rows : [
            { css: "header", cols: [
                { view: "button", type: "icon", icon: "arrow-left", label: mcService.Lang(63), width: 100, css: "headerBtn noBG noUpperCase", // "63" :"Назад",
                    click: $scope.close
                },

                { id: "profileHeaderDisplayName", view: "template", template: $scope.displayName, autoheight: true, borderless: true, height: 34, css: "byCenter scrollMenu selectable"},

                { view: "button", id: "saveBtn" + $scope.Name, type: "icon", icon: "check", label: mcService.Lang(77), click: $scope.saveProfile, // "77" : "Сохранить",
                    css: "headerBtn", width: 140, hidden: true
                }
            ]},

            { height: 3, css: "whiteBG"},

            { view: "scrollview", css: "scrollMenu", body: {
                id: "profileBody" + $scope.Name, css: "scrollMenu", rows: [
                    { height: 4},

                    { cols: [
                        { width: 15},

                        { rows: [ // infoMainDesc
                            { view:"text",  id: 'upUIN',       value:$scope.data.UIN,       height: textHeight, labelWidth: labelMainW, label:"UIN:", disabled: true},
                            { view:"text",  id: 'upFirstName', value:$scope.data.FirstName, height: textHeight, labelWidth: labelMainW, label:mcService.Lang(68),on:{onTimedKeyPress: $scope.change }, disabled: $scope.notMyProfile}, // "68" : "Имя:",
                            { view:"text",  id: 'upMiddleName',value:$scope.data.MiddleName,height: textHeight, labelWidth: labelMainW, label:mcService.Lang(69),on:{onTimedKeyPress: $scope.change }, disabled: $scope.notMyProfile}, // "69" : "Отчество:",
                            { view:"text",  id: 'upLastName',  value:$scope.data.LastName,  height: textHeight, labelWidth: labelMainW, label:mcService.Lang(70),on:{onTimedKeyPress: $scope.change }, disabled: $scope.notMyProfile}, // "70" : "Фамилия:",
                            { view:"text",  id: 'upNick',      value:$scope.data.Nick,      height: textHeight, labelWidth: labelMainW, label:mcService.Lang(57),on:{onTimedKeyPress: $scope.change }, disabled: $scope.notMyProfile}, // "57" : "Ник:",
                            { view:"select",id: 'upSex',       value:$scope.data.Sex,       height: textHeight, labelWidth: labelMainW, label:mcService.Lang(71),on:{onChange: $scope.change }, disabled: $scope.notMyProfile, options:_sex, css: "selectProp"}, // "71" : "Пол:",
                            { view:"text",  id: 'upEmail',     value:$scope.data.Email,     height: textHeight, labelWidth: labelMainW, label:mcService.Lang(72),on:{onTimedKeyPress: $scope.change }, disabled: $scope.notMyProfile},  // "72" : "Основной E-mail:",

                            { cols: [
                                {},
                                { view:"button",id: 'upChangePwdBtn', value: mcLang(662),      height: textHeight, hidden: $scope.notMyProfile, width: 200, disabled: !$scope._rights.QAllowChangeAccountPassword, click: $scope.createChangePwdWindow},  // "662":"Изменить пароль",
                            ]}
                            //{ view: "text",   id: 'upRole',      value:$scope.data.Role,      labelWidth: labelWidth, label: mcService.Lang(73), disabled: true, css: "color54"}
                        ]},

                        infoMainFoto,

                        { width: 5}
                    ]},

                    { height: 7},

                    { cols: [
                        { width: 5},
                        
                        { view : "accordion", multi: true, borderless: true, css: "headerAccordion", rows : [
                            { header:mcService.Lang(127), headerHeight: 35, collapsed: false, css: "prInp", body: { rows: [ // "127": "Информация о компании (организации)",
                                { height: 7 },

                                __textInput('WorkCompanyName',118, 126, $scope.change, $scope.notMyProfile),
                                __textInput('WorkPosition',    '', 124, $scope.change, true, !$scope.data.WorkPosition),
                                __textInput('WorkDept',        '', 123, $scope.change, true, !$scope.data.WorkDept),
                                __textInput('WorkOffice',     118, 122, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.WorkOffice),
                                __textInput('WorkPhone',      118, 121, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.WorkPhone),
                                __textInput('WorkFax',        118, 120, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.WorkFax),
                                __textInput('WorkPager',      118, 119, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.WorkPager),

                                __textInput('WorkCity',         118, 164, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.WorkCity),
                                __textInput('WorkStreetAddress',118, 163, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.WorkStreetAddress),
                                __textInput('WorkState',        118, 165, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.WorkState),
                                __textInput('WorkZIP',          118, 166, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.WorkZIP),

                                { view:"select",
                                    id           : 'upWorkCountry',
                                    value        : $scope.data.WorkCountry,
                                    labelPosition: 'left',
                                    labelWidth   : labelWidth,
                                    label        : mcLang(167), // "167":"Страна",
                                    on           : { onChange: $scope.change },
                                    options      : mcService.GetCountriesList(),
                                    css          : "noBGAll",
                                    disabled     : $scope.notMyProfile,
                                    hidden       : $scope.notMyProfile && !$scope.data.WorkZIP
                                },

                                __textInput('WorkWWW',     118, 178, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.WorkWWW),

                                { height: 10}
                            ]}},

                            { header: mcService.Lang(148), headerHeight: 35, collapsed: false, css: "prInp", body: { rows: [ // "148":"Личная информация",
                                { height: 7},

                                { view:"datepicker",
                                    id           : 'upBirthday',
                                    value        : $scope.data.Birthday,
                                    labelPosition: 'left',
                                    labelWidth   : labelWidth,
                                    placeholder  : mcLang(118),
                                    label        : mcLang(149) + ":", on:{ onChange: $scope.change }, // "149":"Дата рождения:",
                                    disabled     : $scope.notMyProfile,
                                    hidden       : $scope.notMyProfile && !$scope.data.Birthday,
                                    suggest      : {
                                        type:"calendar",
                                        body: {
                                            minDate: new Date("1900.01.01"),
                                            maxDate: new Date("3000.01.01")
                                        }
                                    }
                                },

                                { view:"select",
                                    id           : 'upMaritalStatus',
                                    value        : $scope.data.MaritalStatus,
                                    labelPosition: 'left',
                                    labelWidth   : labelWidth,
                                    label        : mcLang(150) + ":",
                                    on           : { onChange: $scope.change },
                                    options      : mcService.GetMaritalStatusList(),
                                    css          : "noBGAll",
                                    disabled     : $scope.notMyProfile,
                                    hidden       : $scope.notMyProfile && !$scope.data.MaritalStatus
                                },

                                __textInput('HomePhone',        118, 159, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.HomePhone),
                                __textInput('HomeFax',          118, 160, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.HomeFax),
                                __textInput('HomeCellular',     118, 161, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.HomeCellular),
                                __textInput('HomeStreetAddress',118, 163, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.HomeStreetAddress),
                                __textInput('HomeCity',         118, 164, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.HomeCity),
                                __textInput('HomeState',        118, 165, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.HomeState),
                                __textInput('HomeZIP',          118, 166, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.HomeZIP),

                                { view:"select",
                                    id           : 'upHomeCountry',
                                    value        : $scope.data.HomeCountry,
                                    labelPosition: 'left',
                                    labelWidth   : labelWidth,
                                    label        : mcLang(167), // "167":"Страна",
                                    on           : { onChange: $scope.change },
                                    options      : mcService.GetCountriesList(),
                                    css          : "noBGAll",
                                    disabled     : $scope.notMyProfile,
                                    hidden       : $scope.notMyProfile && !$scope.data.HomeCountry
                                },

                                __textInput('HomeWWW',          118, 168, $scope.change, $scope.notMyProfile, $scope.notMyProfile && !$scope.data.HomeWWW),

                                { height: 10}
                            ]}}
                        ]},

                        { width: 11}
                    ]},

                    { height: 7},

                    {
                        id      : "upAddInfo",
                        view    : "list",
                        padding : 14,
                        template: $scope.tempSystemInfo,
                        css     : "lineHeight18 monospaceAll selectable",
                        data    : $scope.data ? $scope.data.Net : [],
                        type    : {height: 170},
                        autoheight: true,
                    },

                ]}
            }
        ]
    });

    _view = $$($scope.Name);

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

    $scope.uFoto     = $$("upFoto");
    $scope.upAddInfo = $$("upAddInfo");
    $scope.saveBtn   = $$("saveBtn" + $scope.Name);
    $scope.profileHeaderDisplayName = $$("profileHeaderDisplayName");
    $scope.fotoEditBtns = $$("fotoEditBtns");
    $scope.upChangePwdBtn  = $$("upChangePwdBtn");

    return _view;
}
function userProfileController($scope, $rootScope){
    "use strict";

    $scope.Name        = mcConst.dataModels.UserProfile;
    $scope.userData    = null;
    $scope.noImage     = '<img id="upFotoCanvas" src="' + mcConst.imagesPath.all + 'noimage.png" border="0">';
    $scope.displayName = "";
    $scope._rights     = {
        QUserDetailsChange: false,
        QAllowChangeAccountPassword: false
    };

    var _view     = null;
    var restoreTool = null;

    function setTitle(){
        $scope.profileHeaderDisplayName.define('template', $scope.displayName);
        $scope.profileHeaderDisplayName.refresh();
    }

    // ==== Change Password Form ==========================================

    $scope.createChangePwdWindow = function () {
        if ($$('upChangePwdWnd')) {
            $$('upChangePwdWnd').destructor();
        }

        function changePwdText() {
            var elem = this.getFormView().elements;

            if (mcService.trim(elem['password'].getValue()) !== "" && mcService.trim(elem['passwordOld'].getValue()) !== "") {
                elem["save"].enable();
            } else {
                elem["save"].disable();
            }
        }

        $scope.pwdDialog = webix.ui({
            view     : "window",
            position : "center",
            width    : 300,
            id       : "upChangePwdWnd",
            head     : { cols:[
                    { width: 42},
                    { view: "label",  label:'<div class="byCenter toUpper">' + mcLang(662) + "</div>"}, // // "662":"Изменить пароль",
                    { view: "button", type: 'icon', width: 42, icon: 'times', css: "button_transparent", click: $scope.cancelPwdChange }
                ]},
            modal    : true,
            body     : {
                view : 'form',
                padding: 5,
                rules: {
                    "passwordOld" : webix.rules.isNotEmpty,
                    "password"    : webix.rules.isNotEmpty
                },
                elements : [
                    {
                        view       : "text",
                        placeholder: mcService.Lang(663), // "663":"Старый пароль",
                        type       : 'password',
                        name       : 'passwordOld',
                        required   : "true",
                        height     : 28,
                        on         : {
                            onChange       : changePwdText,
                            onTimedKeyPress: changePwdText
                        }
                    },
                    { height: 28, cols: [
                        {
                            view       : "text",
                            placeholder: mcService.Lang(664), // "664":"Новый пароль",
                            type       : 'password',
                            name       : 'password',
                            required   : "true",
                            on         : {
                                onChange       : changePwdText,
                                onTimedKeyPress: changePwdText
                            }
                        }, {
                            view       : "button",
                            tooltip    : mcLang({id: 1816, from: "admin"}), // "1816":"Сгенирировать пароль",
                            width      : 32,
                            type       : "icon",
                            css        : "iconNoPadding",
                            icon       : "key",
                            click      : $scope.generateUserPwd
                        }
                    ]},
                    {
                        view       : "checkbox",
                        height     : 28,
                        labelWidth : 0,
                        labelRight : mcService.Lang({id: 1817, from: "admin"}), // "1817":"Показать пароль",
                        name       : "showpwd",
                        css        : "finger",
                        on         : { onChange: $scope.showHidePwd }
                    },
                    { height: 28, cols : [
                        {},
                        {
                            view  : "button",
                            value : mcService.Lang(77), // "77" :"Сохранить",
                            name  : "save",
                            disabled: true,
                            click : $scope.changePwd
                        }
                    ]}
                ]
            },
            on: { onShow: $scope.onShowNewUserRegForm, onHide: $scope.onHideNewRegForm }
        });

        $scope.pwdDialog.show();
    };

    $scope.generateUserPwd = function () {
        this.getFormView().elements['showpwd'].setValue(1);
        this.getFormView().elements['password'].setValue(mcService.RandomHash(12, true));
    };

    $scope.onShowNewUserRegForm = function(){
        var Self   = $scope.pwdDialog;
        var pwd    = Self.getBody().elements['passwordOld'];
        var pwdNew = Self.getBody().elements['passwordOld'];

        pwdNew.$setValue('');

        pwd.$setValue('');
        pwd.focus();

        $rootScope.hotKeyDispatcher.addPreset(Self.config.id, [{
            key:  mcConst.keyCodes.enter,
            func: function(){
                $scope.changePwd.apply(Self);
            }
        },{
            key:  mcConst.keyCodes.esc,
            lockPrev: true,
            func: function(){
                Self.hide();
            }
        }], document);
    };

    $scope.onHideNewRegForm = function(){
        $rootScope.hotKeyDispatcher.removePreset(this.config.id);
    };

    $scope.showHidePwd = function (val) {
        var pwdOld = this.getFormView().elements['passwordOld'];
        var pwd    = this.getFormView().elements['password'];

        if (!val){
            pwdOld.define('type', 'password');
            pwd   .define('type', 'password');
        } else {
            pwdOld.define('type', 'text');
            pwd   .define('type', 'text');
        }

        pwdOld.refresh();
        pwd.refresh();
    };

    $scope.changePwd = function(){
        var form     = $scope.pwdDialog.getBody();
        var isValid  = form.validate();
        var pwdNew   = form.elements['password'];
        var pwdOld   = form.elements['passwordOld'];
        var pwdNewVal= pwdNew.getValue();
        var pwdOldVal= pwdOld.getValue();

        if (isValid && pwdNewVal && pwdOld) {
            // if (needChangePWD){
            //     needChangePWD = false;
            //
            //     $scope.applyChange($scope.data);
            // }

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_set_account_password,
                mcConst.SessionID,

                pwdOldVal,
                pwdNewVal
            ]);

            // webix.message({ type:"default", text: mcService.Lang(101) , expire: mcConst.errMessageExpire}); // "101": "Пароль успешно изменен!",

            setTimeout(function () {
                form.getTopParentView().hide();

                pwdNew.setValue('');
            }, 10);
        }
    };

    $scope.cancelPwdChange = function(){
        $scope.pwdDialog.getChildViews()[1].elements['password'].setValue('');
        $scope.pwdDialog.getChildViews()[1].elements['passwordOld'].setValue('');
        $scope.pwdDialog.getChildViews()[1].elements['showpwd'].setValue('');

        $scope.pwdDialog.hide();
    };

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

    $scope.registerHotKeys = function () {
        $rootScope.hotKeyDispatcher.addPreset($scope.Name, [{
            key   : mcConst.keyCodes.esc,
            lockPrev: true,
            func  : function closeProfile() {
                $scope.close();
            }
        }], document, true);
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset($scope.Name);
    };

    $scope.close = function () {
        if (_view && _view.isVisible()){
            switch ($rootScope.GetChatType()) {
                case $rootScope.chatAliases.UID: case $rootScope.chatAliases.UIN:
                    $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.ChatFrame, 'd' + $rootScope.GetChatTypeID()]);
                    break;

                default:
                    $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [restoreTool]);
            }

            restoreTool = null;
        }
    };

    $scope.drawFoto = function(fotoData){
        if (fotoData && fotoData.CRC32){
            $scope.uFoto.define('template', '<img src="data:image/jpg;base64,' + fotoData.Foto +'">');
        } else {
            $scope.uFoto.define('template', $scope.noImage);
        }

        $scope.uFoto.refresh();
    };

    $scope.clearFoto = function(){
        webix.confirm({
            type  : "confirm-warning",
            text  : mcService.Lang(553), // "553":"Удалить фотографию?",
            ok    : mcService.Lang(519), // "519":"Удалить",
            cancel: mcService.Lang(33),  // "33" :"Отмена",
            callback:function(yes){
                if (yes){
                    $rootScope.$broadcast('SendCMDToServer', [
                        mcConst._CMD_.cs_set_uin_foto,
                        mcConst.SessionID,
                        ""
                    ]);

                    $$('upFoto').define('template', '<img id="upFotoCanvas" src="" border="0">');
                    $$('upFoto').refresh();

                    webix.message(mcService.Lang(554)); // "554":"Фото удалено",
                }
            }
        });
    };

    $scope.uploadFoto = function(item){
        var file = item.file;
        var type = item.type.toLowerCase();
        var res  = true;

        if(file && (type == "jpg" || type == "png")){
            $rootScope.$broadcast(mcConst.lockInterface, []);

            var reader = new FileReader(); // We need to use a FileReader to actually read the file.

            reader.onload = function(event){
                $$('upFoto').define('template', '<canvas  id="upFotoCanvas" height="' + mcConst.userFoto.h + '" width="' + mcConst.userFoto.w + '"></canvas>');
                $$('upFoto').refresh();

                var img = new Image();

                img.src = event.target.result;

                img.onload = function(){
                    var _canvas = document.createElement('canvas');
                    var resized = document.getElementById('upFotoCanvas');

                    _canvas.width  = img.width;
                    _canvas.height = img.height;

                    if ((_canvas.height <= mcConst.userFoto.h) && (_canvas.width <= mcConst.userFoto.w)){
                        resized.width  = _canvas.width;
                        resized.height = _canvas.height;
                    } else
                    if (_canvas.width >= _canvas.height) {
                        resized.width  = mcConst.userFoto.w;
                        resized.height = _canvas.height / (_canvas.width / mcConst.userFoto.w);
                    } else {
                        resized.width  = _canvas.width / (_canvas.height / mcConst.userFoto.h);
                        resized.height = mcConst.userFoto.h;
                    }

                    var ctx = _canvas.getContext('2d');

                    ctx.fillStyle = '#fff';
                    ctx.fillRect(0, 0, _canvas.width, _canvas.height);
                    ctx.drawImage(img, 0, 0, img.width, img.height);

                    canvasResize(_canvas, resized, function(){
                        $rootScope.$broadcast('SendCMDToServer', [
                            mcConst._CMD_.cs_set_uin_foto,
                            mcConst.SessionID,
                            resized.toDataURL('image/jpeg').replace('data:image/jpeg;base64,', '')
                        ]);

                        $rootScope.$broadcast('hide' + mcConst.lockInterface, []);
                    });
                };
            };

            reader.onerror = function(){ // err
                $rootScope.$broadcast('ErrorMsg', ['client', mcService.Lang(555) + file.name]); // "555":"Ошибка чтения файла: ",

                $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

                res = false;
            };

            reader.readAsDataURL(file);
        } else {
            $rootScope.$broadcast('ErrorMsg', ['client', mcService.Lang(555) + file.name]); // "555":"Ошибка чтения файла: ",

            $rootScope.$broadcast('hide' + mcConst.lockInterface, []); // unlock

            res = false;
        }

        if (!res) {
            return res;
        }
    };
    
    $scope.saveProfile = function(){
        $scope.data.Birthday = mcService.formatDate($scope.data.Birthday, 'dd.mm.yyyy');

        $rootScope.$broadcast('SendCMDToServer' , [
            mcConst._CMD_.cs_set_uin_info,
            mcConst.SessionID,
            JSON.stringify($scope.data),

            function (data) {
                data.forEach(function(user) {
                    if (user.UIN == mcConst.UserInfo.UIN){
                        $scope.displayName = mcConst.UserInfo.Nick = user.DisplayName;
                    }
                });

                webix.message(mcLang(117)); // "117": "Изменения сохранены",

                if (!$scope.notMyProfile){
                    $scope.saveBtn.hide();
                }

                $rootScope.$broadcast(window._messages_.confUsers.updateConfUserInfo, [mcConst.UserInfo.UIN, {
                    DisplayName : $scope.displayName
                }]);

                $rootScope.$broadcast(window._messages_.chatWrapper.changeMyName, [$scope.displayName]);

                $rootScope.$broadcast(window._messages_.dialogsList.confUpdateUserInfo, [{
                    UIN         : mcConst.UserInfo.UIN,
                    DisplayName : $scope.displayName
                }]);

                setTitle();
            }
        ]);
    };

    $scope.change = function(){
        if (!$scope.notMyProfile) {
            var id  = this.data.id.toString().replace(/^up/, '');
            var val = this.getValue();

            if (mcService.isString(val)){
                val = mcService.trim(val.replace(/\u2022|\u0002|\u0003|\u000b|\r|\n/g, ""));

                if (!val && id === "Nick"){
                    
                } else {
                    $scope.data[id] = val;
                }
            } else {
                $scope.data[id] = val;
            }

            $scope.saveBtn.show();
        }
    };

    $scope.getData = function(uin){
        var task = new mcService.TaskList();

        task.AddTask(function () {
            $rootScope.$broadcast('SendCMDToServer' , [
                mcConst.lockInterface,
                mcConst._CMD_.cs_get_uin_info,
                mcConst.SessionID,
                uin,

                function (data) {
                    $scope.data = data;

                    task.Next();
                }
            ]);
        });

        if (uin != mcConst.UserInfo.UIN) {
            task.AddTask(function () {
                $rootScope.$broadcast('SendCMDToServer' , [
                    mcConst.lockInterface,
                    mcConst._CMD_.cs_get_user_computer_info,
                    mcConst.SessionID,

                    uin,

                    function (data) {
                        $scope.data.Net = data.Data;

                        task.Next();
                    }
                ]);
            });
        }

        task.AddTask(function () {
            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_get_uin_foto,
                mcConst.SessionID,
                $scope.data.UIN,

                function (foto) {
                    $scope.show();

                    $scope.drawFoto(foto);

                    $scope.registerHotKeys();
                }
            ]);
        });

        task.Run();
    };

    $scope.tempSystemInfo = function (obj){
        return mcService.myReplaceFormated(
            "<pre>" +
                "Client    : #{Client}<br>" +
                "MAC       : #{MAC}<br>" +
                "CID       : #{CID}<br>" +
                "NetName   : #{NetName}<br>" +
                "Reflink   : #{Reflink}<br>" +
                "IP        : #{IP}<br>" +
                "UserAgent : #{UserAgent}<br>" +
                "HardwareID: #{HardwareID}<br>" +
            "</pre>",
            {
                Client    : obj.Client,
                MAC       : obj.MAC,
                CID       : obj.CID,
                NetName   : obj.NetName,
                Reflink   : obj.Reflink,
                IP        : "<a href='http://geoiplookup.net/ip/" + obj.IP + "' target='_blank'>" + obj.IP + "</a>",
                UserAgent : obj.UserAgent,
                HardwareID: obj.HardwareID
            }
        );
    };

    $scope.show = function(){
        $scope.notMyProfile = $scope.data.UIN != mcConst.UserInfo.UIN || !$scope._rights.QUserDetailsChange;
        $scope.displayName  = $scope.data.DisplayNick || mcConst.UserInfo.Nick;

        _view = initUserProfile($scope);

        Object.keys($scope.data).forEach(function (item) {
            var elem = $$('up' + item);

            if (elem && elem.setValue){
                elem.setValue($scope.data[item]);

                // if (["CompNetName", "IP", "MAC", "LastAccess", "UIN"].indexOf(item) === -1){
                //     elem[$scope.notMyProfile ? "disable" : "enable"]();
                // }
            }
        });

        $scope.saveBtn.hide();

        $scope.fotoEditBtns[$scope.notMyProfile ? "hide" : "show"]();

        setTitle();
    };

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



    $scope.$on('close' + $scope.Name, function() {
        $scope.close();
    });

    $scope.$on('hide' + $scope.Name, function(){
        $scope.removeHotKeys();

        _view.hide();
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        restoreTool = $rootScope.GetChatType();

        if (mcService.checkNoRestoreTool(restoreTool)){
            restoreTool = null;
        }

        $scope.wndSize = $rootScope.wndSize;

        $scope.getData(args[1] || mcConst.UserInfo.UIN);
    });

    $rootScope.$broadcast('windowResize', [function(w, h){
        if (isMobile && _view){
            _view.define('height', h);
            _view.resize();
            _view.resizeChildren();
        }
    }]);

    $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
        $scope._rights.QUserDetailsChange   = mcService.checkRights(mcConst._CMD_.RS.QUserDetailsChange);
        $scope._rights.QAllowChangeAccountPassword   = mcService.checkRights(mcConst._CMD_.RS.QAllowChangeAccountPassword);

        $scope.upChangePwdBtn && $scope.upChangePwdBtn[$scope._rights.QAllowChangeAccountPassword ? "disable" : "enable"]();
    });

}
function initAdminFrame($scope){
    "use strict";

    var view = $$($scope.Name);

    if (!view){
        $$($scope.container).addView({
            id   : $scope.Name,
            view : "iframe",
            css  : "iFrameFit",
            src  : $scope.adminURL + $scope.getToken($scope.adminURL, mcConst.Lang),
            on   : {
                // onAfterLoad: $scope.onLoadKanban
            }
        });

        view = $$($scope.Name);
        
        view.showFrame = function () {
            view.define('width', undefined);
            view.resize();

            mcService.showFrame($scope.Name);
        };

        view.hideFrame = function () {
            view.define('width', '0');
            view.resize();

            mcService.hideFrame($scope.Name);
        };
    } else {
        view.showFrame();
    }

    return view;
}
function adminController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.Admin;

    var iFrame     = null;
    var goUrl      = null;
    var closeTimer = null;
    var closeAfter = 90000;
    var viewContainer = null;

    $scope.token    = null;
    $scope.adminURL = "";

    function setAdminUrl(url) {
        if (!$scope.token){
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_create_token,
                mcConst.SessionID,

                $rootScope.chatAliases.Admin.toLowerCase(),
                $scope.adminURL,

                function (data) {
                    $scope.token    = data.Token;
                    $scope.adminURL = url || $scope.adminURL; //(mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasAdmin + "/");

                    if (!iFrame.config.src) {
                        iFrame.define("src", $scope.adminURL + $scope.getToken($scope.adminURL, mcConst.Lang));
                    }
                }
            ]);
        } else {
            goUrl = url.indexOf($scope.token) > 0 ? url : (url + $scope.getToken(url, mcConst.Lang));

            if (iFrame && url.indexOf(mcService.getLocalHostPath($rootScope.isWebClient) === 0)){
                iFrame.define("src", goUrl);

                goUrl = null;
            } else {
                console.warn("Wrong URL for iFrame:" + goUrl);
            }
        }
    }

    function closeAdmin(){
        clearTimeout(closeTimer);

        closeTimer = setTimeout(function () {
            $scope.token    = null;
            $scope.adminURL = "";

            viewContainer && iFrame && viewContainer.removeView($scope.Name);

            iFrame = null;
        }, closeAfter);
    }

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

    $scope.getToken = function (url, lang) {
        var srch = url.indexOf("?") === -1 && url.indexOf("#") === -1 ? "?" : "&";

        return $scope.token
            ? srch +
              "token=" + $scope.token +
              (lang ? "&lang=" + lang : "")
            : "";
    };

    $scope.registerHotKeys = function () {
        // $rootScope.hotKeyDispatcher.addPreset(mcConst.dataModels.Admin, [{
        //     key   : mcConst.keyCodes.enter,
        //     func  : function () {
        //     }
        // }], document);
    };

    $scope.removeHotKeys = function () {
        // $rootScope.hotKeyDispatcher.removePreset(mcConst.dataModels.Admin);
    };

    $scope.show = function(){
        iFrame = initAdminFrame($scope);

        viewContainer = $$($scope.container);

        clearTimeout(closeTimer);
        closeTimer = null;
    };

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

    var _msg = window._messages_.admin = {
        openAdminUrl  : 'openAdminUrl',
        closeAdmin    : 'closeAdmin'
    };

    $scope.$on(_msg.openAdminUrl, function (e, args) {
        setAdminUrl.apply(null, args);
    });

    $scope.$on(_msg.closeAdmin, function () {
        if (iFrame){
            $scope.token = null;
            $scope.adminURL = null;

            iFrame.define('src', "");
        }
    });

    $scope.$on('hide' + $scope.Name, function(){
        $scope.removeHotKeys();

        if (iFrame && iFrame.isVisible()) {
            iFrame.hideFrame();
        }

        closeAdmin();
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        if (!$scope.adminURL) {
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_create_token,
                mcConst.SessionID,

                $rootScope.chatAliases.Admin.toLowerCase(),
                $scope.adminURL,

                function (data) {
                    $scope.token = data.Token;

                    $scope.show();

                    if (!iFrame.config.src) {
                        $scope.adminURL = mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasAdmin + "/";

                        iFrame.define("src", $scope.adminURL + $scope.getToken($scope.adminURL, mcConst.Lang));
                    }
                }
            ]);
        }

        $scope.show();
    });
}
function initForumFrame($scope){
    "use strict";

    var view = $$($scope.Name);

    if (!view){
        $$($scope.container).addView({
            id   : $scope.Name,
            view : "iframe",
            css  : "iFrameFit",
            src  : $scope.forumURL + $scope.getToken($scope.forumURL, mcConst.Lang),
            on   : {
                // onAfterLoad: $scope.onLoadKanban
            }
        });

        view = $$($scope.Name);
        
        view.showFrame = function () {
            view.define('width', void(0));
            view.resize();

            mcService.showFrame($scope.Name);
        };

        view.hideFrame = function () {
            view.define('width', '0');
            view.resize();

            mcService.hideFrame($scope.Name);
        };
    } else {
        view.showFrame();
    }

    return view;
}
function forumController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.Forum;

    var iFrame     = null;
    var goUrl      = null;
    var closeTimer = null;
    var closeAfter = 50000;
    var viewContainer = null;

    $scope.token    = null;
    $scope.forumURL = "";

    function setForumUrl(url) {
        if (!$scope.token){
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_create_token,
                mcConst.SessionID,

                $rootScope.chatAliases.Forum.toLowerCase(),
                $scope.forumURL,

                function (data) {
                    $scope.token    = data.Token;
                    $scope.forumURL = url || $scope.forumURL;

                    if (!iFrame.config.src) {
                        // iFrame.define("src", $scope.forumURL + $scope.getToken($scope.forumURL));
                        iFrame.define("src", $scope.forumURL.replace("#", "?token=" + $scope.token + "#"));
                    }
                }
            ]);
        } else {
            goUrl = url.indexOf($scope.token) > 0 ? url : (url + $scope.getToken(url, mcConst.Lang));

            if (iFrame && url.indexOf(mcService.getLocalHostPath($rootScope.isWebClient) === 0)){
                iFrame.define("src", goUrl);

                goUrl = null;
            } else {
                console.warn("Wrong URL for iFrame:" + goUrl);
            }
        }
    }

    function closeForum(){
        clearTimeout(closeTimer);

        closeTimer = setTimeout(function () {
            $scope.token    = null;
            $scope.forumURL = "";

            viewContainer && iFrame && viewContainer.removeView($scope.Name);

            iFrame = null;
        }, closeAfter);
    }

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

    $scope.getToken = function (url, lang) {
        var srch = url.indexOf("?") === -1 && url.indexOf("#") === -1 ? "?" : "&";

        return $scope.token
            ? srch +
            "token=" + $scope.token +
            (lang ? "&lang=" + lang : "")
            : "";
    };

    $scope.registerHotKeys = function () {
        // $rootScope.hotKeyDispatcher.addPreset(mcConst.dataModels.Admin, [{
        //     key   : mcConst.keyCodes.enter,
        //     func  : function () {
        //     }
        // }], document);
    };

    $scope.removeHotKeys = function () {
        // $rootScope.hotKeyDispatcher.removePreset(mcConst.dataModels.Admin);
    };

    $scope.show = function(){
        iFrame = initForumFrame($scope);

        viewContainer = $$($scope.container);

        clearTimeout(closeTimer);
        closeTimer = null;
    };

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

    var _msg = window._messages_.forum = {
        openForumUrl  : 'openForumUrl',
        closeForum    : 'closeForum'
    };

    $scope.$on(_msg.openForumUrl, function (e, args) {
        setForumUrl.apply(null, args);
    });

    $scope.$on(_msg.closeForum, function () {
        if (iFrame){
            $scope.token = null;
            $scope.forumURL = null;

            iFrame.define('src', "");
        }
    });

    $scope.$on('hide' + $scope.Name, function(){
        $scope.removeHotKeys();

        if (iFrame && iFrame.isVisible()) {
            iFrame.hideFrame();
        }

        closeForum();
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        if (!$scope.forumURL) {
            $rootScope.SendCMDToServer([
                mcConst._CMD_.cs_create_token,
                mcConst.SessionID,

                $rootScope.chatAliases.Forum.toLowerCase(),
                $scope.forumURL,

                function (data) {
                    $scope.token = data.Token;

                    $scope.show();

                    if (!iFrame.config.src) {
                        $scope.forumURL = mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasForum + "/";

                        iFrame.define("src", $scope.forumURL + $scope.getToken($scope.forumURL, mcConst.Lang));
                    }
                }
            ]);
        }

        $scope.show();
    });
}
function initCreateConfView($scope){
    "use strict";

    var view = $$($scope.Name);

    if (!view){
        $$($scope.container).addView({
            id   : $scope.Name,
            rows : [
                { view: "template", template: mcLang(688), autoheight: true, borderless: true, height: 34, css: "byCenter header borderBottom"}, //  "688":"Создание конференции",

                { height: 10 },

                { cols: [
                    { gravity: 1, minWidth: 10 },

                    { maxWidth: 600, gravity: 10, rows: [
                        { id: "crtConfType", view: "richselect", value: mcConst._CMD_.confType.STANDARD, readOnly: true,
                            on: {
                                onChange: function (val) {
                                    if (val === mcConst._CMD_.confType.PASSWORD){
                                        $scope.crtPwdWrap.show();
                                    } else {
                                        $scope.crtPwdWrap.hide();
                                    }
                                }
                            },
                            options: [
                                { id: mcConst._CMD_.confType.STANDARD, value: mcLang(689) }, // "689":"Обычная конференция",
                                { id: mcConst._CMD_.confType.PASSWORD, value: mcLang(690) }, // "690":"Конференция закрытая паролем",
                                { id: mcConst._CMD_.confType.HIDDEN,   value: mcLang(691) }  // "691":"Невидимая конференция",
                            ]
                        },

                        { id: "crtConfName",  view: "text", placeholder: mcLang(692)}, // "692":"Название конференции",

                        { id: "crtPwdWrap", hidden: true, cols: [
                            { id: "crtConfPass",  view: "text", placeholder: mcLang(697), type: "password"}, // "697":"Пароль конференции",
                            { view: "toggle", width: 35, offIcon: "eye", onIcon: "eye-slash",  type:"icon", css: "iconNoPadding noBG", tooltip: mcLang(699) + "/" + mcLang(698).toLowerCase(), // "698":"Скрыть", "699":"Показать",
                                on: { onChange: function (val) {
                                    var pwd = $scope.crtConfPass;

                                    if (val) {
                                        pwd.define('type', 'text');
                                    } else {
                                        pwd.define('type', 'password');
                                    }

                                    pwd.refresh();
                                    pwd.getInputNode().setAttribute("autocomplete", "new-password");
                                }}
                            }
                        ]},

                        { id: "crtConfTopic", view: "text", placeholder: mcLang(680)}, // "680":"Тема",

                        { height: 20 },

                        { cols: [
                            { gravity: 1, minWidth: 10},
                            { maxWidth: 600, gravity: 10, view: "label", label: mcLang(694), css: "byCenter"}, // "694":"Учасники конференции",
                            { gravity: 1, minWidth: 10}
                        ]},

                        { height: 20 },

                        { id: "crtSearchField", view: "text", placeholder: mcLang(695), borderless: true, keyPressTimeout: 400, on: { // "695":"Поиск пользователей...",
                            onTimedKeyPress: $scope.findUsers,
                            onKeyPress     : $scope.findUsersNav
                        }},

                        { cols: [
                            { width: 3},

                            { rows: [
                                {
                                    view    : "list",
                                    id      : "findList",
                                    select  : true,
                                    height  : 150,
                                    css     : "borderBottomLeft listBG noPadding",
                                    template: $scope.cnView,
                                    hidden  : true,
                                    on      : {
                                        onItemDblClick: $scope.addFindUser
                                    }
                                },

                                { id: "crtNoResultLabel", view: "label", textAlign: "center", label: mcLang(696), hidden: true }, // "696":"-- Нет подходящих результатов --",

                                { height: 2 },

                                {
                                    id       : "crtConfUsersList",
                                    css      : "borderBottomLeft noPadding lHeight26 showOnHover",
                                    view     : "list",
                                    drag     : "target",
                                    borderless: false,
                                    // select   : true,
                                    template : $scope.cnView,
                                    onClick  : $scope.actions,
                                    on       : {
                                        onBeforeDrop: function (dropData) {
                                            if (dropData && dropData.start) {
                                                var user = mcService.Marge({}, dropData.from.getItem(dropData.start));

                                                if (user && user.UIN != 0){
                                                    user.id = user.UIN;
                                                    
                                                    $scope.addUser(user);
                                                }
                                            }

                                            return false;
                                        },
                                        onItemDblClick: function (uin) {
                                            uin && $scope.crtConfUsersList.remove(uin);
                                        }
                                    }
                                }
                            ]},
                        ]},

                        { cols: [
                            { view: "button", value: mcLang(33), maxWidth: 200, css: "button_warning warningBtnBG", click: function () { // "33" :"Отмена",
                                $scope.close(true);
                            }},
                            {},
                            { view: "button", value: mcLang(693), maxWidth: 200, css: "button_success accessBtnBG", click: $scope.createConf} // "693":"Создать",
                        ]}
                    ]},

                    { gravity: 1, minWidth: 10 }
                ]},

                { height: 10}
            ]
        });

        view = $$($scope.Name);

        $scope.crtConfName      = $$("crtConfName");
        $scope.crtConfType      = $$("crtConfType");
        $scope.crtConfTopic     = $$("crtConfTopic");
        $scope.crtConfUsersList = $$("crtConfUsersList");
        $scope.crtSearchField   = $$("crtSearchField");
        $scope.crtPwdWrap       = $$("crtPwdWrap");
        $scope.crtConfPass      = $$("crtConfPass");

        // $scope.findResult = webix.ui({
        //     view : "context",
        //     id   : "findResult" + $scope.Name,
        //     head : false,
        //     css  : "noUpperCase",
        //     padding: 0,
        //     width: 350,
        //     body :
        // });

        $scope.findResultList   = $$("findList");
        $scope.crtNoResultLabel = $$("crtNoResultLabel");

    } else {
        view.show();
    }

    return view;
}

function createConfController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.CreateConf;

    var view         = null;
    var currentQuery = "";
    var findText     = "";
    var currentRes   = "";
    var currentID    = null;
    var restoreTool  = null;

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

    function findUsers(query){
        if (query && currentQuery !== query){
            currentQuery = query;

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst._CMD_.cs_find_users,
                mcConst.SessionID,

                query,
                "DISPLAYNAME,STATE,UIN,SEX,EMAIL",
                15,

                function (res) {
                    currentRes = res;

                    delete currentRes.Fields;

                    if (currentRes[mcConst.UserInfo.UIN]) {
                        delete currentRes[mcConst.UserInfo.UIN];
                    }

                    currentRes = mcService.convertObjToArray(currentRes, "UIN");
                    
                    currentRes.forEach(function (user) {
                        user.Name  = user.DISPLAYNAME;
                        user.onlineState = user.STATE;
                        user.id    = user.UIN;
                    });

                    if (currentRes.length){
                        $scope.findResultList.clearAll();
                        $scope.findResultList.parse(currentRes);
                        $scope.findResultList.show();
                        $scope.crtNoResultLabel.hide();

                        currentID = $scope.findResultList.getFirstId();

                        $scope.findResultList.select(currentID);
                    } else {
                        $scope.findResultList.hide();
                        $scope.crtNoResultLabel.show();
                    }
                }
            ]);
        }
    }

    function nextUser(){
        if (currentRes.length){
            if (currentID === $scope.findResultList.getLastId()) {
                currentID = $scope.findResultList.getFirstId();
            } else {
                currentID = $scope.findResultList.getNextId(currentID);
            }

            $scope.findResultList.select(currentID);
            $scope.findResultList.showItem(currentID);
        }
    }

    function prevUser(){
        if (currentRes.length){
            if (currentID === $scope.findResultList.getFirstId()) {
                currentID = $scope.findResultList.getLastId();
            } else {
                currentID = $scope.findResultList.getPrevId(currentID);
            }

            $scope.findResultList.select(currentID);
            $scope.findResultList.showItem(currentID);
        }
    }

    function clearFindField() {
        $scope.crtSearchField.setValue("");
        $scope.crtSearchField.$setValue("");

        $scope.findResultList.hide();

        setTimeout(function () {
            $scope.findUsers();
        }, 100);
    }

    function addUserToList(){
        if (currentRes.length){
            var user = $scope.findResultList.getSelectedItem();

            if (user) {
                $scope.addUser(user);

                clearFindField();

                $scope.crtSearchField.focus();
            }
        }
    }

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

    $scope.addFindUser  = addUserToList;

    $scope.cnView = function(obj) {
        var isWebSup = obj.Name.indexOf("(websup)") === 0;
        var rColor   = $rootScope.colorPalette[obj.UIN % $rootScope.colorPalette.length];
        var name     = obj.Name.replace("(websup)", "<span class='bolder brown'>W</span>");
        var email    = obj.EMAIL ? ", " + obj.EMAIL : "";

        return mcService.myReplaceFormated(
            "<span title='#{itemTitle}'>" +
                "<span class='dialogItem nowrap' uin='#{uin}' style='margin-left: 2px;'>" +
                    "<span class='circleAvatarSmall shadow2px boxShadow listAvatar' style='background-color: ##{rColor};'>#{icon}</span>" +
                    "#{name}" +
                    "<span class='font90 grayLight'>#{email} (UIN: #{uin})</span>" +
                "</span>" +
                "<span class='crtRemoveUser needShowList toRightInList' title='#{delTitle}' uin='#{uin}'>#{trash}</span>" +
            "</span>",
            {
                uin      : obj.UIN,
                rColor   : rColor,
                icon     : isWebSup ? mcService.getTwoLettersName(obj.Name.replace("(websup)", "")) : mcService.getTwoLettersName(name),
                name     : name,
                itemTitle: obj.Name.replace("(websup)", "") + email + " (UIN: " + obj.UIN + ")",
                isOffline: obj.onlineState == mcConst.states.offline ? 0.35 : 1,
                email    : email,
                trash    : mcService.insertIco("fa-trash"),
                delTitle : mcLang(519)                            // "519":"Удалить",
            }
        );
    };

    $scope.actions = {
        "crtRemoveUser": function (ev) {
            var element = ev.target;
            var uin     = element.getAttribute('uin') ? element.getAttribute('uin') : element.parentElement.getAttribute('uin');

            if (uin){
                $scope.crtConfUsersList.remove(uin);
            }
        }
    };

    $scope.findUsers    = function () {
        var newFindText = mcService.trim($scope.crtSearchField.getValue());

        if (newFindText && newFindText !== findText){
            findText = newFindText;

            findUsers(findText);
        } else
        if (newFindText === ""){
            findText     = "";
            currentQuery = "";
            currentRes   = [];
            currentID    = 0;

            $scope.findResultList.hide();
            $scope.crtNoResultLabel.hide();
        }
    };

    $scope.findUsersNav = function (code, e) {
        if (code == mcConst.keyCodes.adown){
            nextUser();

            e.preventDefault();
        } else
        if (code == mcConst.keyCodes.aup){
            prevUser();

            e.preventDefault();
        } else
        if (code == mcConst.keyCodes.enter){
            addUserToList();
        } else
        if (code == mcConst.keyCodes.esc){
            clearFindField();
        }
    };

    $scope.addUser = function(user){
        if (!$scope.crtConfUsersList.getItem(user.id)){
            $scope.crtConfUsersList.add(user);
        }
    };

    $scope.registerHotKeys = function () {
        $rootScope.hotKeyDispatcher.addPreset($scope.Name, [{
            key   : mcConst.keyCodes.esc,
            lockPrev: true,
            func  : function closeSettings() {

            }
        }], document, true);
    };

    $scope.createConf = function(){
        var name = $scope.crtConfName.getValue();
        var pass = $scope.crtConfPass.getValue();
        var topic= $scope.crtConfTopic.getValue();
        var type = $scope.crtConfType.getValue();
        var users= $scope.crtConfUsersList.serialize();

        if (name && (type === mcConst._CMD_.confType.STANDARD || type === mcConst._CMD_.confType.HIDDEN || (type === mcConst._CMD_.confType.PASSWORD && pass))){
            users = users.map(function (user, i) {
                return user.UIN;
            });

            $rootScope.$broadcast('SendCMDToServer', [
                mcConst.lockInterface,
                mcConst._CMD_.cs_create_txt_channel,
                mcConst.SessionID,

                name,
                topic,
                pass,
                type,
                users.join(','),

                function (info) {
                    $scope.close(true);
                    
                    $rootScope.$broadcast('OpenConf', [info.UID, pass]);
                }
            ]);
        } else  
        if (type === mcConst._CMD_.confType.PASSWORD && !pass) {
            webix.alert(mcLang(701)); // "701":"Установите пароль для конференции",
        } else {
            webix.alert(mcLang(700)); // "700":"Название конференции не может быть пустым",
        }
    };

    $scope.close = function(needClear){
        if (view && view.isVisible()) {
            if (needClear) {
                $scope.crtConfUsersList.clearAll();
                $scope.crtConfName     .setValue("");
                $scope.crtConfTopic    .setValue("");

                clearFindField();
            }

            view.hide();

            $rootScope.$broadcast(window._messages_.dialogsCtrl.disableContactsDD);

            if (needClear) {
                switch ($rootScope.GetChatType()) {
                    case $rootScope.chatAliases.UID:
                    case $rootScope.chatAliases.UIN:
                        $rootScope.$broadcast(window._messages_.dialogsCtrl.selectDialog,     [$rootScope.GetChatTypeID()]);
                        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.ChatFrame, 'd' + $rootScope.GetChatTypeID()]);
                    break;

                    default:
                        $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [restoreTool]);
                }
            }

            restoreTool = null;
        }
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset($scope.Name);
    };

    $scope.show = function(){
        view = initCreateConfView($scope);

        // bakCss = $$($scope.container).getNode().className;

        $$($scope.container).getNode().className = "webix_view noMargin webix_layout_line";

        $scope.crtConfName.focus();
    };

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

    var _msg = window._messages_.CreateConf = {

    };

    // $scope.$on(_msg.changeUserProfile, function (e, args) {
    //     mcConst.ClientSettings = mcService.Marge(mcConst.ClientSettings, args[0]);
    //
    //     $rootScope.$broadcast(window._messages_.clientData.saveClientSettings);
    //
    //     $scope.getData();
    // });

    $scope.$on('hide' + $scope.Name, function(){
        // $scope.removeHotKeys();

        $scope.close();
     });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = mcConst.containers.center;

        restoreTool = $rootScope.GetChatType();

        $scope.show();

        // $scope.registerHotKeys();
    });
}

function initImportantFrame($scope){
    "use strict";

    var view = $$($scope.Name);

    if (!view){
        $$($scope.container).addView({
            id: $scope.Name,
            css: "whiteBG",
            rows: [
                {
                    view  : "label",
                    id    : "ntfLabel",
                    label : "",
                    height: 38,
                    css   : "borderBottom"
                },

                { cols : [
                    {
                        id      : "ntfList" + $scope.Name,
                        view    : "list",
                        width   : 200,
                        css     : "noBGColor findBackground lineHeight18 dialogs_list hiddenScrollMenu thinScrollBar ntfItems", // scrollMenu
                        borderless: true,
                        select  : true,
                        drag    : "source",
                        template: $scope.lstView,
                        scroll  : "y",
                        type    : {
                            headerHeight:30,
                            height: 44
                        },
                        onContext: {},
                        data : $scope.__list || [],
                        on: {
                            // onItemClick   : $scope.clickOnDialogItem,
                            onAfterSelect : $scope.selectNotification,
                            // onBeforeSelect: $scope.beforeSelect,
                            // onItemDblClick: $scope.removePrivateDialog,
                            // onBeforeDrag  : $scope.checkDrag
                        }
                    },

                    { padding: 10, rows: [
                        { height: 2 },
                        { id: "ntfContent", view: "template", borderless: true, css: "flexContainer" }
                    ]}
                ]}
            ]
        });

        view = $$($scope.Name);
        
        // view.showFrame = function () {
        //     view.define('width', undefined);
        //     view.resize();
        //
        //     mcService.showFrame($scope.Name);
        // };
        //
        // view.hideFrame = function () {
        //     view.define('width', '0');
        //     view.resize();
        //
        //     mcService.hideFrame($scope.Name);
        // };

        $scope.ntfList    = $$("ntfList" + $scope.Name);
        $scope.ntfContent = $$("ntfContent");

    } else {
        view.show();
    }

    return view;
}
function importantController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.Important;

    var view      = null;

    $scope.__list = []
    $scope.__ntf  = {};

    function lang(){
        return mcConst.Lang === "uk" ? "UA" : mcConst.Lang.toUpperCase();
    }

    // %firstnameordislayname% - имя или displayname клиента, если имя не указано
    // %id% - mychatclient%ver%-%license%-%servid
    // %ver% - версия клиента, например, 8.6.3
    // %servid% - id сервера MyChat
    // %license% - лицензия, free или comm
    
    // =====================================
    // =====================================

    $scope.registerHotKeys = function () {
        // $rootScope.hotKeyDispatcher.addPreset(mcConst.dataModels.Admin, [{
        //     key   : mcConst.keyCodes.enter,
        //     func  : function () {
        //     }
        // }], document);
    };

    $scope.removeHotKeys = function () {
        // $rootScope.hotKeyDispatcher.removePreset(mcConst.dataModels.Admin);
    };

    $scope.selectNotification = function (id) {
        var text = $scope.__ntf[id].MsgBodyJSON[lang()];

        if (text !== undefined) {
            try {
                text = text
                        .replace(/\<a\s/gi, "<a target='_blank'")
                        .replace("%firstnameordisplayname%", mcConst.UserInfo.DisplayName)
                        .replace("%id%"                    , `mychatclient${$rootScope.clientSysInfo.Ver}-${mcConst.UserInfo.LicenseFree ? 'free' : 'comm'}-${mcConst.UserInfo.ServID}`)
                        .replace("%ver%"                   , $rootScope.clientSysInfo.Ver)
                        .replace("%servid%"                , mcConst.UserInfo.ServID)
                        .replace("%license%"               , mcConst.UserInfo.LicenseFree ? 'free' : 'comm');
            } catch (e) {
                console.err(e.message, console.logLevel.ERROR)
            }
        } else {
            text = "";
        }

        $scope.ntfContent.define("template", "<div class='flexVerticalMiddle font120' style='max-width: 600px; word-break: break-word'> " + text + "</div>");
        $scope.ntfContent.refresh();
    };

    $scope.lstView = function (obj){
        var title = obj.MsgTitleJSON[lang()];
        var icon  = mcService.insertIco("fa-newspaper-o");

        switch (obj.MsgType) {
            case mcConst._CMD_.ntfType.LINUXMACWEB:
            case mcConst._CMD_.ntfType.ANDROID_SETUP:
            case mcConst._CMD_.ntfType.IOS_SETUP:
                icon = mcService.insertIco("fa-bell-o");
            break;
            case mcConst._CMD_.ntfType.UTP:
                icon = mcService.insertIco("fa-handshake-o");
            break;
            case mcConst._CMD_.ntfType.FREE_VER_EXPIRED:
                icon = mcService.insertIco("fa-cart-arrow-down");
            break;

            case mcConst._CMD_.ntfType.COMMON:
                
            break;
            case mcConst._CMD_.ntfType.GAME:

            break;
            case mcConst._CMD_.ntfType.NEED_SUBSCRIPTION_2MONTHS:

            break;
            case mcConst._CMD_.ntfType.NEED_SUBSCRIPTION_1MONTH:

            break;
            case mcConst._CMD_.ntfType.NEED_SUBSCRIPTION_2WEEKS:

            break;
            case mcConst._CMD_.ntfType.SUBSCRIPTION_EXPIRED:

            break;
        }

        return mcService.myReplaceFormated(
            "#{icon}<span class='brown'>" +
                "#{title}" +
            "</span>",
            {
                title: title,
                icon : icon
            }
        );
    };

    $scope.show = function(){
        $scope.__ntf  = {};
        $scope.__list = $rootScope.$broadcast(window._messages_.usersSmallInfo.getNotifications);
        $scope.__list.forEach(function (item) {
            $scope.__ntf[item.id] = item;
        })
        
        view = initImportantFrame($scope);
    };

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

    var _msg = window._messages_.important = {

    };

    $scope.$on('hide' + $scope.Name, function(){
        $scope.removeHotKeys();

        if (view && view.isVisible()) {

            view.hide();
        }
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        $scope.show();
    });
}
function initTerminal ($scope){
    "use strict";

    var _view = $$($scope.Name);

    if (!_view){
        $$($scope.container).addView({
            id  : $scope.Name,
            rows: [
                { view: "template", template: "Terminal", autoheight: true, borderless: true, height: 34, css: "byCenter header"},

                { id: "__terminalWrapper", css: "bgNormal terminal_scroll selectable", template: $scope.getTemplate()}
            ]
        });

        _view = $$($scope.Name);

        $scope.terminalWrapper = $$("__terminalWrapper");

        $scope.terminalWrapper.getNode().addEventListener("click", $scope.focusToTerminal);

        mcService.disableContextMenu(_view);

        $scope.createTerminal();
    } else {
        _view.show();
    }

    return _view;
}
function terminalController($scope, $rootScope){
    "use strict";

    $scope.Name = mcConst.dataModels.Terminal;

    var restoreTool = null;
    var view        = null;
    var _terminal   = null;

    function init(){
        if (!_terminal){
            _terminal = {
                prefix: mcConst.UserInfo.InternalNick + " >> ",

                inputName: "__terminal-input",
                input    : null,

                mirrorName: "__terminal-input-mirror",
                mirror    : null,

                terminalName: "__terminal-output-wrapper",
                terminal    : null,

                wrapperName: "__terminal-input-wrapper",
                wrapper    : null,

                cursorName: "__terminal-cursor",
                cursor: null,

                curPosition: 0,
                curWidth: 0,

                viewTraffic: mcConst.traffic_type.off,

                value : "",

                cmdHistory: [],
                cmdHistoryNumb: -1,

                addTerminalLine: function (data){
                    console.log("<< " + data);

                    var pre = document.createElement("pre");

                    pre.innerHTML = "<< " + data;
                    pre.style.marginTop  = "1px";
                    pre.style.marginLeft = "8px";


                    this.terminal.appendChild(pre);
                },

                enableTrafficView: function (_type) {
                    var self = this;

                    $rootScope.$broadcast("traffic_view", [_type]);

                    if (this.viewTraffic === mcConst.traffic_type.off){
                        $rootScope.$on("traffic_log", function (e, args) {
                            var inTraf = args[0];
                            var log    = args[1];
                            
                            self.addTerminalLine((inTraf? ' IN  ' : ' OUT ') + log.replace(mcConst.SessionID, ""));

                            $scope.terminalWrapper.getNode().scrollTo(0, self.wrapper.offsetTop);
                        });
                    }

                    this.viewTraffic = _type;
                },

                disableTrafficView: function (){
                    $rootScope.$off("traffic_log");
                },

                localCommands: {
                    "list": function (){
                        this.addTerminalLine('\n' + Object.keys(_terminal.localCommands).join('\n'));

                        $scope.terminalWrapper.getNode().scrollTo(0, this.wrapper.offsetTop);
                    },

                    "opendevtools": function () {
                        $rootScope.SendCMDToServer([
                            mcConst._CMD_.ce_open_dev_tool
                        ], true);
                    },

                    "ping": function (){
                        var time = new Date().getTime();
                        var self = this;
                        
                        $rootScope.SendCMDToServer([
                            mcConst._CMD_.cs_ping2,
                            mcConst.SessionID,

                            function () {
                                self.addTerminalLine("pong " + (new Date().getTime() - time) + "mc");

                                $scope.terminalWrapper.getNode().scrollTo(0, self.wrapper.offsetTop);
                            }
                        ]);
                    },

                    "clearlocalstorage": function (){
                        !$rootScope.isWebClient && webix.confirm({
                            // title :
                            ok    : mcLang(43), // "43" : "Да",
                            cancel: mcLang(44), // "44" : "Нет",
                            type  :"confirm-error",
                            text  : mcLang(745), // "745":"Вы точно хотите очистить локальное хранилище?",
                            width : 310,
                            callback:function(result){ //setting callback
                                if (result){
                                    $rootScope.Storage.dialogs.remove(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.DIALOG_LIST);
                                }
                            }
                        });
                    },

                    "sendstat": function (){
                        var name = $rootScope.$broadcast(window._messages_.mcStatistics.sendStat);

                        this.addTerminalLine("sended: " + name);

                        $scope.terminalWrapper.getNode().scrollTo(0, this.wrapper.offsetTop);
                    },

                    "clear": function () {
                        var self = this;

                        self.terminal.innerHTML = "";
                    },

                    "cls": function () {
                        var self = this;

                        self.terminal.innerHTML = "";
                    },

                    "deletelocalusersdb": function () {
                        webix.confirm({
                            ok    : mcLang(43), // "43" : "Да",
                            cancel: mcLang(44), // "44" : "Нет",
                            type  :"confirm-error",
                            text  : "Do you want to remove the local users DB and reload client?",
                            width : 310,
                            callback:function(result){ //setting callback
                                if (result){
                                    if (typeof $rootScope.dbConnect !== 'undefined') {
                                        var dbName = $rootScope.dbConnect.dbName;

                                        var deleteRequest = indexedDB.deleteDatabase(dbName);

                                        deleteRequest.onblocked = function () {
                                            console.log("Delete blocked.");
                                        }

                                        deleteRequest.onerror = function () {
                                            console.log("Error deleting the DB");
                                        };

                                        deleteRequest.onsuccess = function () {
                                            console.log("Deleted OK.");

                                            $rootScope.$broadcast(mcConst.lockInterface);
                                            
                                            setTimeout(function () {
                                                location.reload();
                                            }, 777);
                                        };
                                    }
                                }
                            }
                        });

                    },
                    
                    "traffic cmd": function () {
                        this.addTerminalLine('+ Enabled traffic view ONLY CMD');

                        $scope.terminalWrapper.getNode().scrollTo(0, this.wrapper.offsetTop);

                        this.enableTrafficView(mcConst.traffic_type.cmd);
                    },
                    
                    "traffic all": function () {
                        this.addTerminalLine('+ Enabled traffic view VERBOSE');

                        $scope.terminalWrapper.getNode().scrollTo(0, this.wrapper.offsetTop);

                        this.enableTrafficView(mcConst.traffic_type.all);
                    },

                    "traffic close": function () {
                        this.addTerminalLine('- Disable all traffic view');

                        $scope.terminalWrapper.getNode().scrollTo(0, this.wrapper.offsetTop);

                        this.viewTraffic = mcConst.traffic_type.off;
                        
                        $rootScope.$broadcast("traffic_view", [mcConst.traffic_type.off]);

                        this.disableTrafficView();
                    },

                    "reloadclient": function () {
                        webix.confirm({
                            ok    : mcLang(43), // "43" : "Да",
                            cancel: mcLang(44), // "44" : "Нет",
                            type  :"confirm-error",
                            text  : "Soft reboot the client?", // perform a soft client reboot?
                            width : 310,
                            callback:function(result){ //setting callback
                                if (result){
                                    setTimeout(function () {
                                        location.reload();
                                    }, 50);
                                }
                            }
                        });
                    },

                    "hotkeys debug on": function () {
                        $rootScope.hotKeyDispatcher.debug(true);

                        this.addTerminalLine('Hotkeys debugger ENABLED');
                    },

                    "hotkeys debug off": function () {
                        $rootScope.hotKeyDispatcher.debug(false);
                        
                        this.addTerminalLine('Hotkeys debugger DISABLED');
                    },

                    "hotkeys show list": function () {
                        this.addTerminalLine('Hotkeys info was showed in developer tool console');
                        
                        $rootScope.hotKeyDispatcher.showPresetList();
                    }
                },

                remoteCommands: {
                    "ping": function (data) {
                        return [data.Result + " " + ((new Date()).getTime() - data.TimeStamp) + " ms"];
                    }
                },

                keysHandle: function(code){
                    var self = this;
                    var hasSelection = null;

                    if (mcService.inArrayNoStrict(code, ["Shift", "Control", "Alt"]) === -1) {
                        self.curPosition = mcService.getCaretPosition(self.input);

                        switch (code) {
                            case "Enter":
                                self.value       = mcService.trim(self.input.value);
                                self.input.value = "";

                                if (self.value) {
                                    var div = document.createElement("pre");

                                    div.innerHTML         = self.prefix + self.value;
                                    // div.style.marginTop   = "0";
                                    // div.style.marginBottom= "0";

                                    self.terminal.appendChild(div);

                                    var inHistory = mcService.inArrayNoStrict(self.value, self.cmdHistory);

                                    if (inHistory !== -1) {
                                        self.cmdHistory.splice(inHistory, 1);
                                    }

                                    self.cmdHistory.push(self.value);
                                    self.cmdHistoryNumb = self.cmdHistory.length - 1;
                                }

                                self.mirror.innerHTML = self.prefix;

                                self.curPosition = 0;

                                var params = self.value.split(' ');
                                var _cmd   = params.shift().toLowerCase();

                                if (
                                    _cmd === "traffic" ||
                                    _cmd === "hotkeys"
                                ){
                                    _cmd = mcService.trim(self.value);
                                }

                                console.log(_terminal.prefix + _cmd);

                                if (self.localCommands.hasOwnProperty(_cmd)){
                                    self.localCommands[_cmd].apply(self, params);
                                } else
                                if (_cmd !== "") {
                                    self.addTerminalLine("unknown command: " + _cmd);
                                }

                                $scope.terminalWrapper.getNode().scrollTo(0, self.wrapper.offsetTop);
                            break;

                            case "ArrowUp":
                                if (self.cmdHistoryNumb >=0 ){
                                    self.value = self.input.value = self.cmdHistory[self.cmdHistoryNumb];

                                    if (self.cmdHistoryNumb) self.cmdHistoryNumb--;

                                    self.mirror.innerHTML = self.prefix + self.value;
                                }

                                self.curPosition = 0;
                            break;

                            case "ArrowDown":
                                if (self.cmdHistoryNumb < self.cmdHistory.length){
                                    if (self.cmdHistoryNumb < (self.cmdHistory.length - 1)) {
                                        self.cmdHistoryNumb++;

                                        self.value = self.input.value = self.cmdHistory[self.cmdHistoryNumb];

                                        self.mirror.innerHTML = self.prefix + self.value;
                                    } else {
                                        self.value = self.input.value = "";

                                        self.mirror.innerHTML = self.prefix;
                                    }

                                    self.curPosition = 0;
                                }
                            break;

                            default:
                                self.value = self.input.value;

                                self.mirror.innerHTML = self.prefix + self.value;

                                hasSelection = self.input.selectionEnd - self.input.selectionStart; // - self.input.selectionStart

                                self.cursor.style.backgroundColor = "green";

                                $scope.terminalWrapper.getNode().scrollTo(0, self.wrapper.offsetTop);
                        }

                        var pos = self.curPosition * self.curWidth - self.input.value.length * self.curWidth;

                        self.cursor.style.marginLeft = pos + "px";

                        if (hasSelection) {
                            self.cursor.innerHTML             = (new Array(Math.abs(hasSelection) + 1)).join("&nbsp;");
                            self.cursor.style.backgroundColor = "darkgreen";
                        } else

                        if (self.cursor.innerText.length !== 1) {
                            self.cursor.innerHTML = "&nbsp;";
                        }

                        self.wrapper.scrollTo(self.curPosition * self.curWidth, 0);
                    }
                },

                onfocusInput: function(){
                    mcService.setCaretPosition(this.input, this.curPosition);
                },

                onblurInput: function(){
                    this.curPosition = mcService.getCaretPosition(this.input);
                },

                blinkCursor: function () {
                    var self = this;

                    var selection = window.getSelection().toString();

                    if (selection){
                        mcService.copyTextToClipboard(selection);
                    }

                    if (self.cursor){
                        setTimeout(function () {
                            mcService.hideView(self.cursor);

                            setTimeout(function () {
                                mcService.showView(self.cursor);

                                if (_terminal.input) {
                                    _terminal.input.focus();
                                }
                            }, 350);
                        }, 350);
                    }
                }
            };
        }
    }

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

    $scope.regShowWindow = function(id, link){
        // $rootScope.$broadcast('addShowedWindow', [id, link]);
    };

    $scope.focusToTerminal = function(){
        _terminal.blinkCursor();
    };

    $scope.createTerminal = function(){
        _terminal.input     = document.getElementById(_terminal.inputName);
        _terminal.terminal  = document.getElementById(_terminal.terminalName);
        _terminal.mirror    = document.getElementById(_terminal.mirrorName);
        _terminal.wrapper   = document.getElementById(_terminal.wrapperName);
        _terminal.cursor    = document.getElementById(_terminal.cursorName);

        _terminal.curWidth  = _terminal.cursor.getBoundingClientRect().width;
        // _terminal.curWidth  = _terminal.cursor.offsetWidth;

        _terminal.input.onfocus = function (){
            _terminal.onblurInput();
        };

        _terminal.input.onblur  = function (){
            _terminal.onblurInput();
        };

        _terminal.input.onkeydown = function (e) {
            var code;

            if (e.key !== undefined) {
                code = e.key;
            } else
            if (e.keyIdentifier !== undefined) {
                code = e.keyIdentifier;
            } else
            if (e.keyCode !== undefined) {
                code = e.keyCode;
            }

            setTimeout(function () {
                _terminal.keysHandle(code);
            }, 10);
        };
    };

    $scope.getTemplate = function(){
        return mcService.myReplaceFormated(
            "<div id='#{out}'></div>" +
            "<div id='#{inpWrapper}'>" +
            "   <input id='#{inpName}' type='text' autofocus>" +
            "   <pre id='#{mirror}' class='inline_pre'>#{prefix}</pre><pre id='#{cursor}' class='_terminalCursor'>&nbsp;</pre>" +
            "</div>",{
                cursor    : _terminal.cursorName,
                prefix    : _terminal.prefix,
                inpName   : _terminal.inputName,
                out       : _terminal.terminalName,
                mirror    : _terminal.mirrorName,
                inpWrapper: _terminal.wrapperName
        });
    };

    $scope.registerHotKeys = function () {
        // $rootScope.hotKeyDispatcher.addPreset($scope.Name, [{
        //     key   : mcConst.keyCodes.esc,
        //     lockPrev: true,
        //     func  : function closeTerminal() {
        //         $scope.close();
        //     }
        // }], document, true);
    };
    
    $scope.close = function () {
        switch ($rootScope.GetChatType()) {
            case $rootScope.chatAliases.UID:
            case $rootScope.chatAliases.UIN:
                $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.ChatFrame, 'd' + $rootScope.GetChatTypeID()]);
            break;

            default:
                $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [restoreTool]);
        }

        restoreTool = null;
    };
    
    $scope.show = function(){
        view = initTerminal($scope);

        // mcService.initResize(view, $scope.Name);

        // view.adjust();

        $scope.focusToTerminal();
    };

    $scope.$on('hide' + $scope.Name, function(){
        // $scope.removeHotKeys();

        if (view && view.isVisible()) {
            view.hide();

            // $$($scope.container).getNode().className = bakCss;

            // $rootScope.$broadcast(window._messages_.clientData.setClientTitle);
        }
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        init();

        $scope.container = args[0];

        restoreTool = $rootScope.GetChatType();

        if (mcService.checkNoRestoreTool(restoreTool)){
            restoreTool = null;
        }

        $scope.show();

        // $scope.registerHotKeys();
    });
}

function initChatWrapper($scope){
    "use strict";

    var view = null;

    // === LEFT VIEW =====================================

    var leftView = [
        { id: "mainMenuWrapper" + $scope.Name, cols: [
            { id: "mainMenu" + $scope.Name,  view: "toggle", type: "icon", css: "ico20 button_warning button_transparent noUpperCase shadow1px", //popup: "mainMenuPopup", //
                offIcon: "bars",  onIcon:"close", offLabel: $scope.myName, onLabel: $scope.myName,
                on: { onChange: $scope.showMainMenu }
            },

            { id: "globalSearchBtn" + $scope.Name, view: "button", type: "icon", icon: "search", css: "ico20 button_transparent iconNoPadding white",
                hidden: true, click: $scope.showFind, width: 35, tooltip: mcLang(695)} // "695":"Поиск пользователей",
        ]},

        { id: "searchWrapper" + $scope.Name, hidden: true, css: "findBackground", cols: [
            { id: "searchField" + $scope.Name, view: "text", placeholder: mcLang(48), borderless: true, keyPressTimeout: 400, css: "noBorderAll noBGAll white", on: { // "48" :"Поиск...",
                onTimedKeyPress: $scope.findUsersOnServer,
                onKeyPress     : $scope.findUsersNavigation
            }},

            { id: "searchClose" + $scope.Name, view: "button", type: "icon", icon: "times", css: "ico20 button_transparent iconNoPadding white", click: $scope.closeFind, width: 35} //
        ]},

        { id: $scope.containers.left, css: "scrollMenu noBGColorAll", borderless: true, rows: [ { gravity: 0} ]} // { gravity: 0} - нужен для того, чтобы при закрытии всех инструментов, не схлопывалась по вертикали вьюха с чатом, а то дырка снизу образутся
    ];

    // === RIGHT VIEW ====================================

    var rightView = [
        { id: $scope.containers.right, rows: [
            { gravity: 0 }
        ]}
    ];

    // === TOOLS VIEW ====================================

    var toolBarView = {
        id     : "rightToolbar" + $scope.Name,
        view   : "toolbar",
        padding: 0,
        width  : 38,
        rows   : [
            {  id: "btn_" + mcConst.dataModels.CommonContacts,   view: "toggle", type: "icon", icon: "sitemap", width: 34, css: "iconNoPadding",
                tooltip: mcLang(10),  on: {onItemClick: $scope.clickToolBtn} //  "10" :"Общие контакты",
            },
            {  id: "btn_" + mcConst.dataModels.PersonalContacts, view: "toggle", type: "icon", icon: "users",   width: 34, css: "iconNoPadding",
                tooltip: mcLang(17),  on: {onItemClick: $scope.clickToolBtn} // "17" :"Личные контакты",
            },
            {  id: "btn_" + mcConst.dataModels.HistoryDialogs,   view: "toggle", type: "icon", icon: "history",width: 34, css: "iconNoPadding",
                tooltip: mcLang(578), on: {onItemClick: $scope.clickToolBtn} // "578":"История диалогов",
            },

            {  id: "btn_" + mcConst.dataModels.PrivateInfo,      view: "toggle", type: "icon", icon: "comment", width: 34, css: "iconNoPadding", hidden: true,
                tooltip: mcLang(46),  on: {onItemClick: $scope.clickToolBtn} // "46" :"Приватный разговор",
            },
            {  id: "btn_" + mcConst.dataModels.ConfUserList,     view: "toggle", type: "icon", icon: "comments",width: 34, css: "iconNoPadding", hidden: true,
                tooltip: mcLang(26),  on: {onItemClick: $scope.clickToolBtn} // "26" :"Люди в конференции",
            },
            {  id: "btn_" + mcConst.dataModels.CommonFiles,      view: "toggle", label: "FTP", width: 34, css: "iconNoPadding font95All", hidden: $scope.isWeb,
                tooltip: mcLang(658), on: {onItemClick: $scope.clickToolBtn} // "658":"FTP файлы",
            },
            {  id: "btn_" + mcConst.dataModels.ReceiveFiles,     view: "toggle", type: "icon", icon: "exchange",width: 34, css: "iconNoPadding", hidden: true,
                tooltip: mcLang(588), on: {onItemClick: $scope.clickToolBtn} // "588":"Прием файлов",
            },
            {  id: "btn_qrcode",                                 view: "button", type: "icon", icon: "qrcode",  width: 34, css: "iconNoPadding", hidden: $scope.isWeb,
                tooltip: mcLang(718), on: {onItemClick: $scope.showQrCode} // "718":"QR-код для подключения с телефона",
            },

            { gravity: 0 }
        ]
    };

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

    view = webix.ui({ type: "layout", css: "wrapperChat", rows: [
        { cols:[
            { id: "leftSide", width: 250, css: "bgDarkness noBorder", rows: leftView, minWidth: 200, maxWidth: 300},

            { id: "leftSideResizer", view: "resizer", css: "noBorder bgDarkness", borderless: true},

            { id: $scope.containers.center, css: "noMargin gradient", cols: [ /*{ gravity: 0}*/ ]},

            { id: "rightSideResizer", view: "resizer", css: "noBorder bgMC noMargin", hidden: true, borderless: true}, // bgMC

            { id: "rightSide", width: 250, css: "bgMC noMargin", minWidth: 210, maxWidth: 350, hidden: true, rows: rightView}
        ].concat(toolBarView)}
    ]});

    $scope.rightToolbar = $$("rightToolbar" + $scope.Name);

    $scope.toolBtn.CommonContacts   = $$("btn_" + mcConst.dataModels.CommonContacts);
    $scope.toolBtn.PersonalContacts = $$("btn_" + mcConst.dataModels.PersonalContacts);
    $scope.toolBtn.PrivateInfo      = $$("btn_" + mcConst.dataModels.PrivateInfo);
    $scope.toolBtn.ConfUserList     = $$("btn_" + mcConst.dataModels.ConfUserList);
    $scope.toolBtn.CommonFiles      = $$("btn_" + mcConst.dataModels.CommonFiles);
    $scope.toolBtn.HistoryDialogs   = $$("btn_" + mcConst.dataModels.HistoryDialogs);
    $scope.toolBtn.ReceiveFiles     = $$("btn_" + mcConst.dataModels.ReceiveFiles);
    // $scope.toolBtn.SendBroadcast    = $$("btn_" + mcConst.dataModels.SendBroadcast);

    $scope.mainMenuWrapper  = $$("mainMenuWrapper" + $scope.Name);
    $scope.mainMenu         = $$("mainMenu" + $scope.Name);
    $scope.globalSearchBtn  = $$("globalSearchBtn" + $scope.Name);

    $scope.searchWrapper    = $$("searchWrapper" + $scope.Name);
    $scope.searchField      = $$("searchField" + $scope.Name);
    $scope.searchClose      = $$("searchClose" + $scope.Name);

    $scope.leftSide         = $$("leftSide");
    $scope.leftSideResizer  = $$("leftSideResizer");

    $scope.rightSide        = $$("rightSide");
    $scope.rightSideResizer = $$("rightSideResizer");

    $scope.centerBlock      = $$($scope.containers.center).getNode();
    $scope.rightSideBlock   = $scope.rightSide.getNode();

    return view;
}
function chatWrapperController($scope, $rootScope){
    "use strict";

    $scope.Name             = mcConst.dataModels.ChatWrapper;
    $scope.toolBtn          = {
        isEmpty: function (){
            var res = true;

            Object.keys($scope.toolBtn).forEach(function (name){
                if ($scope.toolBtn[name].isVisible && $scope.toolBtn[name].isVisible()) {
                    res = false;
                }
            });

            return res;
        },
        offAll: function (){
            $scope.toolBtn.CommonContacts  .setValue(false);
            $scope.toolBtn.PersonalContacts.setValue(false);
            $scope.toolBtn.PrivateInfo     .setValue(false);
            $scope.toolBtn.ConfUserList    .setValue(false);
            $scope.toolBtn.CommonFiles     .setValue(false);
            $scope.toolBtn.HistoryDialogs  .setValue(false);
            $scope.toolBtn.ReceiveFiles    .setValue(false);
        }
    };
    $scope.containers       = mcConst.containers;
    $scope.notShowRightSide = false;

    var view                  = null;
    var oldFrame              = null; // mcConst.dataModels.ChatFrame;
    var currentFrame          = mcConst.dataModels.ChatFrame;
    var pinTool               = '';
    var hideRightSideByResize = false;
    var addingWidth           = 0;
    var restoreTool           = null;
    var restoreValue          = 0;
    var findText              = "";
    var toolList              = [
        mcConst.dataModels.CommonContacts,
        mcConst.dataModels.HistoryDialogs,
        mcConst.dataModels.PersonalContacts,
        mcConst.dataModels.PrivateInfo,
        mcConst.dataModels.ConfUserList,
        mcConst.dataModels.ReceiveFiles,
        mcConst.dataModels.Important
    ].concat($rootScope.isWebClient ? [] : [mcConst.dataModels.CommonFiles]);
    var saveWndSizeTimer      = null;
    var menuShowed            = false;
    var _rights               = {
        QAllowFindUsersTool     : false,
        QCommonAllowDialogs     : false,
        QAllowContactsListsTotal: false,
        QShowCommonContactsList : false,
        QFTPPublic              : false,
        QFTPPersonal            : false,
        QViewTextConfMembersList: false
    };
    var optionsIsLoaded       = false;
    var visualStatesList      = [];
    var defaultVisualStates   = function () {
        return {
            leftSide : true,
            rightSide: true,
            menu     : false,
            pin      : false,
            search   : true,
            pinned   : "",
            tools    : {
                CommonContacts  : true,
                PersonalContacts: true,
                PrivateInfo     : false,
                ConfUserList    : true,
                CommonFiles     : true,
                HistoryDialogs  : true,
                ReceiveFiles    : false
            }
        }
    };

    var selectToolList = new function sTool() {
        var data     = {};
        var nowTool  = null;
        var prevTool = null;
        var self     = this;

        self.setValue = function (val, skipOnChange) {
            var new_Tool = data.hasOwnProperty(val) ? val : null;
            var res      = false;

            if (nowTool !== new_Tool) {
                if (nowTool === null && prevTool){
                    nowTool  = new_Tool;
                } else {
                    prevTool = nowTool;
                    nowTool  = new_Tool;
                }

                if (nowTool && self.onChange && nowTool !== prevTool && !skipOnChange) {
                    self.onChange(nowTool, prevTool);
                }

                res = true;
            }

            return res;
        };

        self.defineData = function(_data){
            data = _data;
        };

        self.getValue = function () {
            return nowTool;
        };

        self.getPrevValue = function(){
            return prevTool;
        };

        self.onChange = null
    };

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

    function unPinTool(noSave){
        pinTool = '';

        if (!noSave && optionsIsLoaded) {
            $rootScope.Storage.options.save({
                pinTool : ""
            });
        }
    }

    function PinTool(tool, notSave) {
        pinTool = tool;

        if (!notSave && optionsIsLoaded) {
            $rootScope.Storage.options.save({
                pinTool: tool
            });
        }
    }

    function hideRightSide(noscroll) {
        $scope.rightSide.hide();
        $scope.rightSideResizer.hide();

        !noscroll && $rootScope.$broadcast(window._messages_.chatFrame.scrollDownChat, [$rootScope.GetChatTypeID(), true]);

        if ($scope.toolBtn.isEmpty()){
            $scope.toolBtn.offAll();

            // $scope.notShowRightSide = true;
        }
    }
    
    function showRightSide() {
        if (!$scope.toolBtn.isEmpty()){
            $scope.rightSide.show();
            $scope.rightSideResizer.show();

            $rootScope.$broadcast(window._messages_.chatFrame.scrollDownChat, [$rootScope.GetChatTypeID(), true]);
        }
    }

    function switchToolBtnVisible(tool, pinTool) {
        switch (tool){
            case mcConst.dataModels.PrivateInfo:
                if ($rootScope.GetChatTypeID() === $rootScope.chatAliases.Elisa){
                    $scope.toolBtn.PrivateInfo.hide();
                } else {
                    $scope.toolBtn.PrivateInfo.show();
                }
                
                $scope.toolBtn.ConfUserList.hide();

                paintBG('privateBg');
            break;

            case mcConst.dataModels.ConfUserList:
                $scope.toolBtn.PrivateInfo.hide();
                _rights.QViewTextConfMembersList && $scope.toolBtn.ConfUserList.show();

                paintBG('bgMC');
            break;

            case mcConst.dataModels.CommonContacts:
            case mcConst.dataModels.HistoryDialogs:
            case mcConst.dataModels.PersonalContacts:
            case mcConst.dataModels.CommonFiles:
                // no action
            break;

            case mcConst.dataModels.ReceiveFiles:
                $scope.toolBtn[mcConst.dataModels.ReceiveFiles].show();
            break;

            default:
                if (!restoreTool) {
                    if ($scope.toolBtn.PrivateInfo.isVisible())  restoreTool = mcConst.dataModels.PrivateInfo;
                    if ($scope.toolBtn.ConfUserList.isVisible()) restoreTool = mcConst.dataModels.ConfUserList;
                }

                $scope.toolBtn.PrivateInfo.hide();
                $scope.toolBtn.ConfUserList.hide();

                paintBG();

                if (!pinTool) {
                    hideRightSide();
                }
        }
    }

    function selectToolView(tool, needClick) {
        if (pinTool === '') {
            if (needClick && tool){
                $scope.toolBtn[tool].callEvent("onItemClick");
            } else {
                $scope.changeTool(tool);
            }
        } else {
            switchToolBtnVisible(tool, pinTool);
        }

        if ($scope.toolBtn.isEmpty()){
            hideRightSide();
        }

        $rootScope.$broadcast(window._messages_.chatFrame.focusToEnterChat);
    }

    function toggleRightToolBarByWndSize() {
        var chkWidth = (!hideRightSideByResize ? mcConst.minChatFrameWidth : mcConst.minChatFrameWidth + addingWidth + 40);

        if (!hideRightSideByResize && $scope.centerBlock.offsetWidth <= chkWidth && $scope.rightSide.isVisible()) {
            addingWidth = $scope.rightSideBlock.offsetWidth;

            selectToolView(selectToolList.getValue(), true);

            hideRightSideByResize = true;
        } else
        if (hideRightSideByResize && $scope.centerBlock.offsetWidth > chkWidth) {
            selectToolView(selectToolList.getValue(), true);

            addingWidth = 0;

            hideRightSideByResize = false;
        }

        if (saveWndSizeTimer){
            clearTimeout(saveWndSizeTimer);
        }

        saveWndSizeTimer = setTimeout(function () {
            saveWndSizeTimer = null;

            $rootScope.Storage.saveWndSize();
        }, 200);
    }

    function __config() {
        var data = {};

        $scope.containers[mcConst.dataModels.CommonContacts]   = mcConst.containers.right;
        $scope.containers[mcConst.dataModels.PersonalContacts] = mcConst.containers.right;
        $scope.containers[mcConst.dataModels.PrivateInfo]      = mcConst.containers.right;
        $scope.containers[mcConst.dataModels.ConfUserList]     = mcConst.containers.right;
        $scope.containers[mcConst.dataModels.CommonFiles]      = mcConst.containers.right;
        $scope.containers[mcConst.dataModels.HistoryDialogs]   = mcConst.containers.right;
        $scope.containers[mcConst.dataModels.ReceiveFiles]     = mcConst.containers.right;

        $scope.containers[mcConst.dataModels.Dialogs]          = mcConst.containers.left;
        $scope.containers[mcConst.dataModels.MainMenu]         = mcConst.containers.left;

        $scope.containers[mcConst.dataModels.ChatFrame]        = mcConst.containers.center;
        $scope.containers[mcConst.dataModels.UserProfile]      = mcConst.containers.center;
        $scope.containers[mcConst.dataModels.BBS]              = mcConst.containers.center;
        $scope.containers[mcConst.dataModels.Kanban]           = mcConst.containers.center;
        $scope.containers[mcConst.dataModels.ViewLogs]         = mcConst.containers.center;
        $scope.containers[mcConst.dataModels.Settings]         = mcConst.containers.center;
        $scope.containers[mcConst.dataModels.Terminal]         = mcConst.containers.center;
        $scope.containers[mcConst.dataModels.SendBroadcast]    = mcConst.containers.center;
        $scope.containers[mcConst.dataModels.Admin]            = mcConst.containers.center;
        $scope.containers[mcConst.dataModels.Forum]            = mcConst.containers.center;
        $scope.containers[mcConst.dataModels.HistoryBroadcast] = mcConst.containers.center;
        $scope.containers[mcConst.dataModels.Important]        = mcConst.containers.center;

        data[mcConst.dataModels.CommonContacts  ] = mcService.Lang(10);  //"10" : "Контакты",
        data[mcConst.dataModels.PersonalContacts] = mcService.Lang(17);  //"17" : "Личные контакты",
        data[mcConst.dataModels.PrivateInfo     ] = mcService.Lang(46);  //"46" : "Приватный разговор",
        data[mcConst.dataModels.ConfUserList    ] = mcService.Lang(26);  //"26" : "Люди в конференции",
        data[mcConst.dataModels.CommonFiles     ] = mcService.Lang(567); //"567":"Общие файлы на сервере",
        data[mcConst.dataModels.HistoryDialogs  ] = mcService.Lang(578); //"578":"История диалогов",
        data[mcConst.dataModels.ReceiveFiles    ] = mcService.Lang(588); //"588":"Прием файлов",

        selectToolList.defineData(data);
        selectToolList.onChange = $scope.changeTool;
    }

    function restoringTool(newFrame, _needSelectTool) {
        if (newFrame &&
            [
                mcConst.dataModels.Settings,
                mcConst.dataModels.Terminal,
                mcConst.dataModels.ViewLogs,
                mcConst.dataModels.BBS,
                mcConst.dataModels.UserProfile,
                mcConst.dataModels.Kanban,
                mcConst.dataModels.Admin,
                mcConst.dataModels.Forum,
                mcConst.dataModels.HistoryBroadcast,
                mcConst.dataModels.SendBroadcast,
                mcConst.dataModels.CreateConf,
                mcConst.dataModels.Important
            ].indexOf(newFrame) === -1){

            var nowRestoreTool = _needSelectTool || restoreTool;

            switch (nowRestoreTool){
                case mcConst.dataModels.CommonContacts:
                case mcConst.dataModels.PersonalContacts:
                case mcConst.dataModels.HistoryDialogs:
                    switch ($rootScope.GetChatAlias()){
                        case $rootScope.chatAliases.Conf:
                            _rights.QViewTextConfMembersList && $scope.toolBtn[mcConst.dataModels.ConfUserList].show();

                            paintBG('privateBg');
                        break;

                        case $rootScope.chatAliases.Privat:
                            $scope.toolBtn[mcConst.dataModels.PrivateInfo].show();

                            paintBG('bgMC');
                        break;
                    }
                break;

                default:
                    var __rTool = $scope.toolBtn[nowRestoreTool];

                    if (nowRestoreTool) {
                        __rTool.show();
                    }

                    if (nowRestoreTool && __rTool.getValue() !== restoreValue) {
                        __rTool.callEvent("onItemClick");
                        // selectToolView();
                    } else {
                        selectToolView(nowRestoreTool);
                    }

                    paintBG();
            }
        } else {
            selectToolView();
        }
    }

    function changeCenterView(newFrame, params){
        if (mcService.inArray(newFrame, [
            mcConst.dataModels.Settings,
            mcConst.dataModels.Terminal,
            mcConst.dataModels.ViewLogs,
            mcConst.dataModels.BBS,
            mcConst.dataModels.UserProfile,
            mcConst.dataModels.Kanban,
            mcConst.dataModels.Admin,
            mcConst.dataModels.Forum,
            mcConst.dataModels.HistoryBroadcast,
            mcConst.dataModels.SendBroadcast,
            mcConst.dataModels.Important
        ]) !== -1 || newFrame === null){
            $scope.rightToolbar.hide();

            // unPinTool(true);
        } else
        if (!mcConst.UserInfo.hasInvite) {
            $scope.rightToolbar.show();
        }

        if (newFrame && newFrame !== currentFrame){
            $rootScope.$broadcast('hide' + currentFrame);

            if (!mcService.checkNoRestoreTool(currentFrame)) {
                oldFrame = currentFrame;
            }

            currentFrame = newFrame;

            $rootScope.$broadcast('show' + newFrame, [$scope.containers[newFrame], params]);

            if (!params || params !== "dUIN-0") {
                restoringTool(newFrame, params && mcService.isString(params) && params.indexOf('dUIN') === 0
                    ? mcConst.dataModels.PrivateInfo
                    : (params && mcService.isString(params) && params.indexOf('dUID') === 0 ? mcConst.dataModels.ConfUserList : null));
            }
        } else

        if (!newFrame && oldFrame){
            $rootScope.$broadcast('hide' + currentFrame);

            currentFrame = oldFrame;

            $rootScope.$broadcast('show' + oldFrame, [$scope.containers[oldFrame], params]);

            if (!params || params !== "dUIN-0") {
                restoringTool(oldFrame);
            }
        } else

        if (newFrame === mcConst.dataModels.UserProfile){
            currentFrame = newFrame;

            $rootScope.$broadcast('show' + newFrame, [$scope.containers[newFrame], params]);
        }

        if (newFrame !== mcConst.dataModels.UserProfile && newFrame !== mcConst.dataModels.UserProfile && !mcConst.UserInfo.hasInvite) {
            $rootScope.$broadcast(window._messages_.chatFrame.hideUniBadge);
        }
    }

    function paintBG(_color) {
        var color = _color ? _color : ($rootScope.GetChatType() === "UID" ? "bgMC" : $rootScope.GetChatType() === "UIN" ? "privateBg" : "");
        
        $scope.rightSideResizer.getNode().className = "webix_view webix_resizer_x noBorder noMargin " + color;
        $scope.rightSide.getNode().className = "webix_view noMargin webix_layout_line " + color;
    }

    function hideTool(tool) {
        switch (tool){
            case mcConst.dataModels.ReceiveFiles:
                if ($scope.toolBtn[tool].isVisible()){
                    $scope.toolBtn[tool].setValue(false);
                    $scope.toolBtn[tool].hide();

                    // if (currentToo l === tool){
                    if (selectToolList.getValue() === tool){
                        if ($rootScope.GetChatType() === $rootScope.chatAliases.UIN && $rootScope.GetChatTypeID() !== $rootScope.chatAliases.Elisa){
                            $scope.changeTool(mcConst.dataModels.PrivateInfo);
                        } else
                        if ($rootScope.GetChatType() === $rootScope.chatAliases.UID){
                            $scope.changeTool(mcConst.dataModels.ConfUserList);
                        } else {
                            hideRightSide();

                            restoreTool = selectToolList.getPrevValue();
                        }
                    }
                }
            break;
        }
    }

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

    $scope.registerHotKeys = function () {
        $rootScope.hotKeyDispatcher.addPreset($scope.Name, [{
            key   : mcConst.keyCodes.esc,
            shiftKey: true,
            func  : function () {
                // selectToolView(currentToo l, true);
                selectToolView(selectToolList.getValue(), true);
            }
        }], document);
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset($scope.Name);
    };

    $scope.showFind = function (){
        $scope.mainMenuWrapper.hide();
        $scope.searchWrapper.show();

        $scope.searchField.focus();
    };

    $scope.closeFind = function (){
        $scope.mainMenuWrapper.show();
        $scope.searchWrapper.hide();

        $scope.searchField.setValue("");

        findText = "";

        $rootScope.$broadcast("hide" + mcConst.dataModels.Search);
        $rootScope.$broadcast(window._messages_.chatFrame.focusToEnterChat);
    };

    $scope.findUsersOnServer = function (){
         var newFindText = mcService.trim($scope.searchField.getValue());

        if (newFindText && newFindText !== findText){
            findText = newFindText;
            
            $rootScope.$broadcast('hide' + mcConst.dataModels.Dialogs);
            $rootScope.$broadcast(window._messages_.search.searchUsers, [findText]);
        } else
        if (newFindText === ""){
            findText = "";
            
            $rootScope.$broadcast('hide' + mcConst.dataModels.Search);
            $rootScope.$broadcast('show' + mcConst.dataModels.Dialogs, [$scope.containers[mcConst.dataModels.Dialogs]]);
        }
    };

    $scope.findUsersNavigation = function(code, e){
        if (code == mcConst.keyCodes.adown){
            $rootScope.$broadcast(window._messages_.search.nextUser);
            e.preventDefault();
        } else
        if (code == mcConst.keyCodes.aup){
            $rootScope.$broadcast(window._messages_.search.prevUser);
            e.preventDefault();
        } else
        if (code == mcConst.keyCodes.enter){
            $rootScope.$broadcast(window._messages_.search.openCurrent);
        }
    };

    $scope.showMainMenu = function () {
        var val = $scope.mainMenu.getValue();

        if (val){ // === show menu ===
            menuShowed = false;

            $scope.globalSearchBtn.hide();

            webix.html.addCss( $scope.mainMenu.getNode(), "darkenNavBG");

            webix.html.removeCss( $scope.leftSideResizer.getNode(), "bgDarkness");
            webix.html.addCss   ( $scope.leftSideResizer.getNode(), "darkenNavBG");

            $rootScope.$broadcast('hide' + mcConst.dataModels.Dialogs);
            $rootScope.$broadcast('show' + mcConst.dataModels.MainMenu, [$scope.containers[mcConst.dataModels.MainMenu]]);

            if (!pinTool && [$rootScope.chatAliases.Conf, $rootScope.chatAliases.Privat].indexOf($rootScope.GetChatAlias()) === -1){
                restoreTool = null;
            }
        } else { // === show dialogs ===
            menuShowed = true;

            if (_rights.QAllowFindUsersTool) $scope.globalSearchBtn.show();

            webix.html.removeCss( $scope.mainMenu.getNode(), "darkenNavBG");

            webix.html.addCss   ( $scope.leftSideResizer.getNode(), "bgDarkness");
            webix.html.removeCss( $scope.leftSideResizer.getNode(), "darkenNavBG");

            $rootScope.$broadcast('hide' + mcConst.dataModels.MainMenu);
            $rootScope.$broadcast('show' + mcConst.dataModels.Dialogs, [$scope.containers[mcConst.dataModels.Dialogs]]);
        }

        //$scope.mainMenu.refresh();
    };

    $scope.changeTool = function(tool, oldTool){
        var currentType = $rootScope.GetChatType();

        oldTool = oldTool || selectToolList.getValue();

        if ($rootScope.GetChatTypeID() === $rootScope.chatAliases.Elisa && tool === mcConst.dataModels.PrivateInfo){
            switchToolBtnVisible();
        }

        if (($rootScope.GetChatTypeID() === $rootScope.chatAliases.Elisa && tool === mcConst.dataModels.PrivateInfo) ||
            (tool === mcConst.dataModels.ConfUserList && currentType === $rootScope.chatAliases.UIN) ||
            (tool === mcConst.dataModels.PrivateInfo  && currentType === $rootScope.chatAliases.Conf)){

            if (selectToolList.setValue(oldTool)) {
                $scope.changeToolBtn.apply($scope.toolBtn[oldTool]);
            }
        } else {
            unPinTool();

            selectToolList.setValue(tool, true);

            if (!$scope.notShowRightSide && tool){
                $scope.changeToolBtn.apply($scope.toolBtn[tool]);

                showRightSide();
            }

            switchToolBtnVisible(tool);

            if (tool){
                $rootScope.$broadcast('show' + tool, [$scope.containers[tool]]);
            }
        }

        if ($scope.rightSide.isVisible()){
            hideRightSideByResize = false;
        }

        // view.adjust();
    };

    $scope.changeToolBtn = function(){
        toolList.forEach(function (itm) {
            $scope.toolBtn[itm] && $scope.toolBtn[itm].setValue(false);
        });
        
        if (this && this.setValue){
            this.setValue(true);

            restoreTool  = this.config.id.split('_')[1];
            restoreValue = this.getValue();
        }
    };

    $scope.clickToolBtn = function(){
        var oldTool = selectToolList.getValue();

        restoreTool  = this.config.id.split('_')[1];
        restoreValue = this.getValue();

        if (oldTool === restoreTool && !restoreValue){ // !this.getValue() - unpressed
            hideRightSide();

            $rootScope.$broadcast('hide' + oldTool);

            $scope.notShowRightSide = true;
        } else {
            $scope.notShowRightSide = false;

            $scope.changeTool(restoreTool);
        }

        optionsIsLoaded && $rootScope.Storage.options.save({
            notShowRightSide: $scope.notShowRightSide
        });
    };

    $scope.showQrCode = function (){
        if ($$('checkPwdWnd')) {
            $$('checkPwdWnd').destructor();
        }

        function closePwd(){
            pwdDialog.close();

            $rootScope.hotKeyDispatcher.removePreset("checkPwdWnd");
        }

        $rootScope.hotKeyDispatcher.addPreset("checkPwdWnd", [{
            key   : mcConst.keyCodes.esc,
            func  : closePwd
        }, {
            key   : mcConst.keyCodes.enter,
            func  : _clickOK
        }], document, true);

        function _clickOK(){
            var pwd = pwdDialog.getBody().elements['password'].getValue();

            closePwd();

            if (pwd){
                var clientOptions = {
                    "Addr"      : mcConst.ServerInfo.Host,
                    "Port"      : mcConst.ServerInfo.Port,    // optional, default 2004
                    "AddrAlt"   : mcConst.ServerInfo.AltHost, // optional
                    "PortAlt"   : mcConst.ServerInfo.AltPort, // optional, default 2004
                    "NodePort"  : mcConst.UserInfo.PortNode,
                    "SSL"       : mcConst.UserInfo.HTTPS,
                    "Domain"    : mcConst.UserInfo.Domain,  // optional
                    "Login"     : mcConst.UserInfo.UIN,     // optional
                    "Pwd"       : pwd, // mcConst.LoginInfo.pwd,    // optional
                    "ServerPwd" : mcConst.LoginInfo.servPwd,// optional
                    rm          : mcConst.LoginInfo.rm
                };
                var w   = 400;
                var w_9 = w * .7;
                var h   = 400;
                var h_9 = h * .7;

                $rootScope.$broadcast("sendCMDToElectron", [
                    mcConst._CMD_.ce_encode_qrcode_json,
                    clientOptions,

                    function (args){
                        var opt    = args[0];
                        var servID = args[1];
                        var qrCode = null;
                        var qrWnd  = webix.ui({
                            position: "center",
                            view    : "window",
                            modal   : true,
                            width   : w * 1.1,
                            height  : h * 1.1,
                            cancel  : mcService.Lang(33), // "33": "Отмена",
                            head    : { cols:[
                                    { width: 42 },
                                    { view : "label",  label:'<div class="byCenter toUpper">' + mcService.Lang(718) + "</div>"}, // "718":"QR-код для подключения с телефона",
                                    { view : "button", type: 'icon', width: 42, icon: 'times', css: "button_transparent", click: closeQR}
                                ]},
                            body: { rows: [
                                    { template: "<div class='byCenterAll' style='height: 100%'><div id='__qr_code_mobile__' style='width: " + w_9 + "px; margin: auto;'></div></div>"},
                                    { template:
                                            '<div class="byCenter">' +
                                            '    <span><a href="https://play.google.com/store/apps/details?id=com.nss.mychat">' +
                                            '        <img src="' + mcConst.imagesPath.all + 'google_play.png" style="max-width: 45%;" alt=""></a></span>' +
                                            '    <span><a href="https://appgallery.cloud.huawei.com/ag/n/app/C101957635?channelId=MyChat&id=45933d7021e24f4e966536bef46d4f0e&s=280BB5A195517207757C222D5E55719BBBA3BAE0DC8ECD85D7DC604ADE9E07C9&detailType=0&v=">' +
                                            '        <img src="' + mcConst.imagesPath.all + 'app_gallery.png" style="max-width: 45%;" alt=""></a></span>' +
                                            '</div>',
                                        autoheight: true, padding: 0, borderless: true
                                    },
                                ]},
                            on  : {
                                onShow: function (){
                                    qrCode = new QRCode(document.getElementById("__qr_code_mobile__"), {
                                        text         : "https://mychat-server.com:50000/?id=" + servID + "&o=" + opt,
                                        width        : w_9,
                                        height       : h_9,
                                        colorDark    : "#131313",
                                        colorLight   : "#ffffff",
                                        correctLevel : QRCode.CorrectLevel.H
                                    });
                                }
                            }
                        });

                        function closeQR(){
                            qrCode.clear();
                            qrWnd .close();

                            $rootScope.hotKeyDispatcher.removePreset("__qr_code_mobile__");
                        }

                        $rootScope.hotKeyDispatcher.addPreset("__qr_code_mobile__", [{
                            key   : mcConst.keyCodes.esc,
                            func  : closeQR
                        }], document, true);

                        qrWnd.show();
                    }
                ]);
            }
        }

        var pwdDialog = webix.ui({
            view     : "window",
            position : "center",
            width    : 300,
            id       : "checkPwdWnd",
            head     : false,
            modal    : true,
            body     : {
                view : 'form',
                padding: 5,
                rules: {
                    "password"    : webix.rules.isNotEmpty
                },
                elements : [
                    {
                        view       : "text",
                        placeholder: mcService.Lang(753), // "753":"Ваш пароль",
                        type       : 'password',
                        name       : 'password',
                        required   : "true",
                        height     : 28
                    },
                    { height: 28, cols : [
                        {}, {
                            view  : "button",
                            value : mcService.Lang(13), // "13" :"Ок",
                            click : _clickOK
                        }, {}
                    ]}
                ]
            }, on: {
                onShow: function (){
                    pwdDialog.getBody().elements['password'].focus();
                }
            }
        });

        pwdDialog.show();
    };

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

    var _msg = window._messages_.chatWrapper = {
        showHideMenu      : "showHideMenu",
        selectTool        : "selectTool",
        changeCenterView  : "changeCenterView",
        pinTool           : "pinTool",
        unPinTool         : "unPinTool",
        hideTool          : "hideTool",
        isVisibleRightSide: "isVisibleRightSide",
        changeMyName      : "changeMyName",
        closeGlobalSearch : "closeGlobalSearch",
        showGlobalSearch  : "showGlobalSearch",
        hideLeftSide      : "hideLeftSide",
        showLeftSide      : "showLeftSide"
    };

    $scope.$on(_msg.hideLeftSide, function () {
        $scope.leftSide.hide();
        $scope.leftSideResizer.hide();
    });

    $scope.$on(_msg.showLeftSide, function () {
        $scope.leftSide.show();
        $scope.leftSideResizer.show();
    });

    $scope.$on(_msg.closeGlobalSearch, function () {
        $scope.closeFind();
    });

    $scope.$on(_msg.showGlobalSearch, function () {
        $scope.showFind();
    });

    $scope.$on(_msg.hideTool, function (e, args) {
        hideTool.apply(null, args);
    });

    $scope.$on(_msg.changeMyName, function (e, args) {
        var name = args[0];

        $scope.mainMenu.define("offLabel", name);
        $scope.mainMenu.define("onLabel", name);
        $scope.mainMenu.refresh();
    });

    $scope.$on(_msg.isVisibleRightSide, function () {
        return $scope.rightSide.isVisible();
    });

    $scope.$on(_msg.showHideMenu, function (e, args) {
        if (args === void(0)){
            $scope.mainMenu.toggle();
        } else {
            $scope.mainMenu.setValue(args[0]);
        }
    });

    $scope.$on(_msg.pinTool, function(e, args){
        PinTool.apply(null, args);
    });

    $scope.$on(_msg.unPinTool, function(){
        unPinTool();
    });

    $scope.$on('hide' + $scope.Name, function(e, args){
        if (view) {
            view.hide();

            if (args[0]){
                view.destructor();
                view = null;
            }
        }
    });

    $scope.$on('show' + $scope.Name, function(){
        $scope.wndSize = mcService.getWindowSize();
        $scope.myName  = mcConst.UserInfo.Nick;
        $scope.isWeb   = $rootScope.isWebClient;

        __config();

        view = initChatWrapper($scope);

        $rootScope.$broadcast('show' + mcConst.dataModels.Dialogs, [$scope.containers[mcConst.dataModels.Dialogs]]);

        $scope.registerHotKeys();

        var left  = $scope.leftSide.getNode();
        var right = $scope.rightSide.getNode();

        left.ondragover = right.ondragover = left.ondrop = right.ondrop =
            $scope.centerBlock.ondragover = $scope.centerBlock.ondragleave = function(e) {
                if (e.dataTransfer.files.length){
                    e.preventDefault();
                }
            };

        $scope.centerBlock.ondrop = function(e) {
            var dataTransfer = e.dataTransfer;

            if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
                $rootScope.$broadcast(window._messages_.downloadUpload.on_file_drop, [dataTransfer, e]);
            }

            e.preventDefault();
        };
    });

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

    $scope.$on(_msg.selectTool, function(e, args){
        selectToolView.apply(null, args);
    });

    $scope.$on(_msg.changeCenterView, function(e, args){
        changeCenterView.apply(null, args);
    });

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

    $rootScope.$broadcast('windowResize', [function(w, h){
        if (view){
            if (isMobile){
                $scope.wndSize.width = w;
                $scope.wndSize.height = h;

                view.define('height', h);
                view.define('width', w);

                $scope.leftSide.define('width', w);
                $$($scope.containers.center).define('width', w);
                $scope.rightSide.define('width', w);

                $scope.leftSide.resize();
                $$($scope.containers.center).resize();
                $scope.rightSide.resize();

                view.resize();
                
                view.getNode().style.top = window.visualViewport.offsetTop + "px";

                setTimeout(function () {
                    $rootScope.$broadcast(window._messages_.chatFrame.scrollDownChat, [$rootScope.GetChatTypeID(), true]);
                }, 50)

            } else {
                toggleRightToolBarByWndSize();
            }

            // view.adjust();
        }
    }]);

    $rootScope.registerRightsCheck('setRights' + $scope.Name, function () {
        _rights.QCommonAllowDialogs         = mcService.checkRights(mcConst._CMD_.RS.QCommonAllowDialogs);
        _rights.QAllowContactsListsTotal    = mcService.checkRights(mcConst._CMD_.RS.QAllowContactsListsTotal);
        _rights.QShowCommonContactsList     = mcService.checkRights(mcConst._CMD_.RS.QShowCommonContactsList);
        _rights.QAllowFindUsersTool         = mcService.checkRights(mcConst._CMD_.RS.QAllowFindUsersTool);
        _rights.QFTPPublic                  = mcService.checkRights(mcConst._CMD_.RS.QFTPPublic);
        _rights.QFTPPersonal                = mcService.checkRights(mcConst._CMD_.RS.QFTPPersonal);
        _rights.QViewTextConfMembersList    = mcService.checkRights(mcConst._CMD_.RS.QViewTextConfMembersList);

        if (!menuShowed) {
            $scope.globalSearchBtn[_rights.QAllowFindUsersTool ? 'show' : 'hide']();
        }

        $scope.toolBtn.CommonContacts  [_rights.QShowCommonContactsList  ? 'show' : 'hide']();
        $scope.toolBtn.PersonalContacts[_rights.QAllowContactsListsTotal ? 'show' : 'hide']();
        $scope.toolBtn.ConfUserList    [_rights.QViewTextConfMembersList ? 'show' : 'hide']();

        if (!mcConst.UserInfo.hasInvite){
            $scope.leftSide                [_rights.QCommonAllowDialogs      ? 'show' : 'hide']();
            $scope.leftSideResizer         [_rights.QCommonAllowDialogs      ? 'show' : 'hide']();
        }

        !_rights.QShowCommonContactsList  && $rootScope.$broadcast('hide' + mcConst.dataModels.PersonalContacts);
        !_rights.QAllowContactsListsTotal && $rootScope.$broadcast('hide' + mcConst.dataModels.CommonContacts);
        !_rights.QViewTextConfMembersList && $rootScope.$broadcast('hide' + mcConst.dataModels.ConfUserList);

        if (!$rootScope.isWebClient){
            $scope.toolBtn.CommonFiles[_rights.QFTPPublic || _rights.QFTPPersonal ? 'show' : 'hide']();
        }

        if (mcConst.UserInfo.hasInvite){
            $scope.toolBtn.HistoryDialogs.hide();

            window.isMobile && $scope.rightToolbar.hide();
        }

        // if (_rights.QViewTextConfMembersList && mcConst.UserInfo.hasInvite){
        //     showRightSide();
        // }

        $rootScope.Storage.options.load(function (info) { // todo: не лучшее место вычитки опций, подумать, куда перенести
            optionsIsLoaded = true;

            if (mcService.isObject(info)) {
                $scope.notShowRightSide       = info.notShowRightSide;
                mcConst.currentDownloadFolder = info.currentDownloadFolder;
                var techID                    = info.TechID || 0;

                $scope.notShowRightSide && hideRightSide(true);

                if (!$scope.notShowRightSide) {
                    toggleRightToolBarByWndSize();
                }

                var backupTool = info.pinTool;

                setTimeout(function () {
                    if (backupTool) {
                        if (selectToolList.setValue(backupTool)) {
                            $scope.changeToolBtn.apply($scope.toolBtn[backupTool]);
                        }

                        $rootScope.$broadcast(window._messages_[backupTool].clickPinBtn, [backupTool]);

                        PinTool(backupTool);
                    }
                }, 100);

                if (techID != mcConst.UserInfo.TechID){
                    setTimeout(function () {
                        $rootScope.$broadcast('SendCMDToServer', [
                            mcConst._CMD_.cs_get_delayed_commands,
                            mcConst.SessionID,

                            techID
                        ]);
                    }, 100);
                }

                $rootScope.Storage.options.save({
                    TechID : mcConst.UserInfo.TechID
                });
            }
        });
    });
}



function mainController($scope, $rootScope, mcCmdParser, mcConnect,/*mcCmdLoop,*/ $http, $location, mcStatistics){
    "use strict";

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

    window.idMcPingTimer = null;

    var pingOptions =  {
        onError : null,
        onStop  : null,
        onWarn  : null
    };

    function StartPingTimer(_time, sID, 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);
            }

            mcCmdParser.runCmd(false, [mcConst._CMD_.sc_error, {
                "ErrNum" : 224, 
                "Params" : [err]
            }]);
        };

        return window.idMcPingTimer;
    }

    function StopPingTimer(err){
        if (window.idMcPingTimer !== null){
            clearInterval(window.idMcPingTimer);

            window.idMcPingTimer = null;

            mcConnect.ClearAllCallBackFunctionByCMD();

            if (err && pingOptions.onError){
                pingOptions.onError(err);
            } else
            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.dropConnectOnLogin = true;

                        cb && cb();
                    }}, {}] }
                ]
            }
        });
    }

    function displayError(txtCode, txt, txtParam, callback, okText){
        if (!errorDialog) {
            errorMessagesIsShowed = true;

            lockWindow && lockWindow.hide();

            var code = ['0','0','0','0'];
            var cLen = txtCode.toString().length - 1;

            for (var i = 3; i >= 0; i--){
                if (cLen < 0) {
                    break;
                }

                code[i] = txtCode.toString()[cLen];
                cLen --;
            }

            code = code.join('');
            
            errorDialog = webix.alert({
                title    : txtCode === -1 ? "" : mcService.myReplaceFormated(
                    "<a class='errorLink' href='#{location}/serverhelp/#{lang}/index.html?mychatclienterrors#{code}.htm' target='_blank'>#{text}</a>", // "11" : "Ошибка #"
                    {
                        location: mcService.getLocalHostPath($rootScope.isWebClient),
                        lang    : mcConst.Lang === "uk" ? "ua" : mcConst.Lang,
                        code    : code,
                        text    : mcService.Lang(11, txtCode) + "&nbsp;" + mcService.insertIco("fa-external-link")

                    }
                ),
                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);
        }

        // webix.message($rootScope.wndSize.width  + " " +  $rootScope.wndSize.height);
        if (isMobile){
            document.getElementsByTagName("body")[0].style.height = parseInt($rootScope.wndSize.height) + "px";
        }
    }

    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 (){
        if (!mcConst.PingTimer)
        mcConst.PingTimer = StartPingTimer(mcConst.PingInterval, mcConst.SessionID, {
            onError : function(){
                if ($rootScope.isWebClient){
                    mcConst.LoggedIn = false;

                    if (!(mcConst.ClientSettings.LogsPrivates && mcConst.ClientSettings.LogsTxtChannels)) {
                        $rootScope.$broadcast('hide' + mcConst.dataModels.ChatWrapper, [true]);
                    }

                    $rootScope.$broadcast('ErrorMsg', ['223', mcService.Error('223'), '', function(){ // "223": "потеряно соединение с сервером",
                        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");
                }
            }
        });
    }

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

    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.dropConnectOnLogin= false;

    // webix.message($rootScope.wndSize.width  + " " +  $rootScope.wndSize.height);

    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) {
        initClientData($scope, $rootScope, sendCMD, mcStatistics);
    }

    // ========= Отправка клиентских логов (browser) на сервер ===========

    if (window.cLog){
        if ($rootScope.isWebClient) {
            cLog(mcConst._CMD_.cLog, sendCMD, true);
        } else {
            cLog(mcConst._CMD_.ce_console_log, sendCMD);
        }
    }

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

    $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 stopProcessRequest = null;

    window._userActions.showUserGeolocation = function (){
        $rootScope.$broadcast(window._messages_.chatFrame.showUserGeoposition);
    };

    window._userActions.openUserProfile = function (uin){
        if (mcService.checkRights(mcConst._CMD_.RS.QAllowViewRemoteUserProfile) && !mcConst.UserInfo.hasInvite){
            $rootScope.$broadcast(window._messages_.chatWrapper.changeCenterView, [mcConst.dataModels.UserProfile, uin]);
            $rootScope.$broadcast("selectTool");
        }
    };
    window._userActions.refreshUserProcess = function () {
        var refIco = this;
        var uin    = $rootScope.GetChatID(); //refIco.getAttribute('uin');

        refIco.className += " fa-spin";

        $rootScope.$broadcast(window._messages_.dialogsList.getActiveProcess, [uin]);

        if (stopProcessRequest){
            clearTimeout(stopProcessRequest);
            stopProcessRequest = null;
        }

        stopProcessRequest = setTimeout(function () {
            stopProcessRequest = null;

            refIco.className = refIco.className.replace(" fa-spin", "");
        }, 5000);
    };

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

    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(e, args){
        if (mcConst.PingTimer) {
            if (mcConst.LoggedIn && mcStatistics && mcStatistics.saveStat){
                mcStatistics.saveStat(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN);
            }

            StopPingTimer.apply(null, args);

            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.hotKeyDispatcher.clearPresetLevel(1); // clear all hot keys above 1 level

                    $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);
                }

                $rootScope.isWebClient && 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.skin.material.listItemHeight = 22;

    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)));
    });

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

    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);
    mcService.addEvent(window.visualViewport, "resize", setWindowSize);
}

"use strict";

if (!window.old_IE) {
    var units   = []
        // .concat(location.protocol === 'file:' ? { module: 'cLog', args: []} : [])
        .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: 'mcEmojiList',      args: ['$rootScope']})
        .concat({ module: 'mcUploadProgress', args: ['$rootScope']})
        .concat(location.protocol === 'file:' ? { module: 'mcStatistics', args: ['$rootScope']} : [])
        .concat({ module: 'mcCmdParser',      args: ['mcConnect',  '$rootScope', 'mcPlaySound', 'mcStatistics']})
        .concat({ module: 'McAudioConf',      args: ['$rootScope']})
        .concat({ module: 'McMessageRender',  args: ['$scope', '$rootScope', 'mcPlaySound', 'mcEmojiList']})
        .concat({ module: 'mcCallManager',    args: ['$scope', '$rootScope', 'mcPlaySound']})
        .concat({ module: 'mcUsersSmallInfo', args: ['$rootScope']})
        .concat({ module: 'mcAvatars',        args: ['$rootScope']})
        .concat({ module: 'mcVoiceRecorder',  args: ['$rootScope']})

        .concat({ module: "mainController",   args: ['$scope', '$rootScope', 'mcCmdParser', 'mcConnect',/*'mcCmdLoop',*/ '$http',
                                                     '$location', 'mcStatistics']})

        .concat({ module: 'loginController',            args: ['$scope', '$rootScope', '$location']})
        .concat({ module: 'commonContactsController',   args: ['$scope', '$rootScope']})
        .concat({ module: 'personalContactsController', args: ['$scope', '$rootScope']})
        .concat({ module: 'chatFrameController',        args: ['$scope', '$rootScope', 'mcPlaySound', 'McMessageRender', 'mcVoiceRecorder', 'mcEmojiList', 'McAudioConf']})
        .concat({ module: 'confUserListController',     args: ['$scope', '$rootScope']})
        .concat({ module: 'confManagerController',      args: ['$scope', '$rootScope']})
        .concat({ module: 'dialogsController',          args: ['$scope', '$rootScope', 'mcPlaySound']})
        .concat({ module: 'mainMenuController',         args: ['$scope', '$rootScope']})
        .concat({ module: 'privateInfoController',      args: ['$scope', '$rootScope']})
        .concat({ module: 'userProfileController',      args: ['$scope', '$rootScope']})
        .concat({ module: 'chatWrapperController',      args: ['$scope', '$rootScope']})
        .concat({ module: 'bbsController',              args: ['$scope', '$rootScope', 'mcPlaySound']})
        .concat({ module: 'kanbanController',           args: ['$scope', '$rootScope']})
        .concat({ module: 'historyDialogsController',   args: ['$scope', '$rootScope']})
        .concat({ module: 'broadcastController',        args: ['$scope', '$rootScope']})
        .concat({ module: 'sendBroadcastController',    args: ['$scope', '$rootScope']})
        .concat({ module: 'historyBroadcastController', args: ['$scope', '$rootScope']})
        .concat({ module: 'adminController',            args: ['$scope', '$rootScope']})
        .concat({ module: 'searchController',           args: ['$scope', '$rootScope']})
        .concat({ module: 'forumController',            args: ['$scope', '$rootScope']})
        .concat({ module: 'createConfController',       args: ['$scope', '$rootScope']})
        .concat({ module: 'importantController',        args: ['$scope', '$rootScope']})
        .concat({ module: 'terminalController',         args: ['$scope', '$rootScope']})

        .concat(location.protocol === 'file:' ? { module: 'ftpFilesController',      args: ['$scope', '$rootScope']} : [])
        .concat(location.protocol === 'file:' ? { module: 'updateController',        args: ['$scope', '$rootScope']} : [])
        .concat(location.protocol === 'file:' ? { module: 'viewLogsController',      args: ['$scope', '$rootScope']} : [])
        .concat(location.protocol === 'file:' ? { module: 'serverManagerController', args: ['$scope', '$rootScope', 'mcCmdParser']} : [])
        .concat(location.protocol === 'file:' ? { module: 'receiveFilesController',  args: ['$scope', '$rootScope']} : [])
        .concat(location.protocol === 'file:' ? { module: 'settingsController',      args: ['$scope', '$rootScope']} : [])

    ;

    window.mcService = servicesController('chat');
    window.mcLang    = mcService.Lang;

    mcComponents.units(units).start();
}


// 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!");
//     }
// }
//
//
// getUserMedia({
//     audio : false,
//     video : {
//         mandatory: {
//             chromeMediaSource  : 'desktop',
//             chromeMediaSourceId: "screen:0:0",
//             minWidth           : 160,
//             minHeight          : 120,
//             maxWidth           : 1920,
//             maxHeight          : 1080,
//             minFrameRate       : 5,
//             maxFrameRate       : 15
//         }
//     }
// }, function successGetMedia(myStream) {
//
//     var vContainer = document.getElementById("thumbsID");
//     vContainer.autoplay  = true;
//     vContainer.srcObject = myStream;
//
//     vContainer.onloadedmetadata = function (e) {
//         vContainer.play()
//     };
// }, function () {
//
// });
