;(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',
        SrvError     : 'SrvError',
        
        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_get_media_conf_user_list             : '01F1', // запит списку учасників медіаконференції, відповідь - sc_media_conf_user_list
        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', // попытка отредактировать своё сообщение в конференции, отправленное только в течение указанного в настройках сервера периода времени

        cs_kanban_get_last_comments_by_stages   : '01FB', // получить список последних комментариев для тасков выбранных этапов, ответ придёт в команде sc_kanban_last_comments_list
        cs_kanban_get_script_list               : '01FC', // получить список скриптов, которые можно назначать для задач в разных этапах. Нужны права для получения списка скриптов (#281)
                                                          // ответка - sc_kanban_script_list, если список не пустой
        cs_kanban_update_task_script_list       : '01FD', // обновить список скриптов для задания. Если список пустой - всё будет удалено
        cs_kanban_get_task_script_list          : '01FE', // получить список скриптов, "привязанных" к указанной задаче. Нужны права для получения списка скриптов (#281)


        cs_set_reaction                         : '020B', // добавить, изменить или удалить реакцию для сообщения. Если поле "What" пустое либо отсутствует - то реакция будет снята
        cs_typing_notify_conf                   : '020C', // отправить уведомление в конференцию про то, что мы что-то набираем

        cs_adm_get_api_keys                     : '020D', // получить список ключей Integration API, ответ - sc_adm_api_keys_list
        cs_adm_set_status_api_key               : '020E', // включить или выключить ключ Integration API, если всё ок - в ответ sc_adm_command_complete с этой командой
        cs_adm_add_api_key                      : '020F', // создать новый ключ Integration API, ответ - sc_adm_api_key_created
        cs_adm_del_api_key                      : '0210', // удалить ключ Integration API, в ответ - sc_adm_command_complete с этой командой
        cs_adm_modify_api_key                   : '0211', // редактирование ключа Integration API, ответ - sc_adm_command_complete с этой командой
        cs_adm_clear_api_keys                   : '0212', // удалить все ключи Integration API, ответ - sc_adm_command_complete с этой командой

        cs_adm_skype_history_upload             : '0215', // только что загружен tar-файл истории скайпа
        cs_adm_skype_history_import             : '0216', // импорт истории для пользователя
        cs_adm_skype_history_import_abort       : '0217', // прервать процесс импорта истории пользователя Skype, не важно, есть сейчас импорт или нет, ошибок не будет,
                                                          // придёт подтверждение - стандартная команда complete

        cs_adm_clear_https_certs                : '0218', // очистить папки https-сертификатов: C:\ProgramData\MyChat Server\cert\ C:\ProgramData\MyChat Server\le\.well-known\acme-challenge\

        cs_remove_private_history               : '0207', // полностью удалить историю сообщений с пользователем, на всех своих устройствах, на устройствах собеседника, а также в серверной базе данных.
                                                          // В диалоге появляется отметка, что вся история удалена, тип сообщения - MSG_TYPE_HISTORY_REMOVED
        cs_remove_conf_history                  : '0208', // полностью удалить историю сообщений в конференции, на всех устройтвах пользователей-членов конференции, а также в серверной базе данных.
                                                          // В диалоге появляется отметка, что вся история удалена, тип сообщения - MSG_TYPE_HISTORY_REMOVED



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

        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_newbies_to_txt_conf             : '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', // список "плохих" слов для фильтра успешно добавлен

        sc_mute_data                            : '8179', // информация про mute-настройки уведомлений в конференциях и приватах для пользователя

        sc_kanban_last_comments_list            : '8178', // список последних комментариев для тасков выбранных этапов, ответ на cs_kanban_get_last_comments_by_stages
        sc_kanban_script_list                   : '817A', // список скриптов для канбан-тасков, ответ на команду cs_kanban_get_script_list. Названия через запятую, текстовая строка
        sc_kanban_task_script_list              : '817B', // список скриптов, "привязанных" к указанной задаче. Ответ на команду cs_kanban_get_task_script_list. Если скриптов нет - пустой массив

        sc_typing_notify_conf                   : '8180', // уведомление о том, что в конференции что-то пишет или отправляет пользователь

        sc_adm_api_keys_list                    : '8182', // список ключей Integration API, ответ на запрос cs_adm_get_api_keys, array of objects
        sc_adm_api_key_created                  : '8183' , // успешно создан новый ключ Integration API, ответ на запрос cs_adm_add_api_key

        sc_adm_skype_history_extracted          : '8186', //
        sc_adm_skype_history_progress           : '8187', // процесс импорта истории пользователя Skype
        sc_adm_skype_history_imported           : '8188', // процесс импорта истории пользователя Skype завершён


        // ==== 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', // перевірка сертифіката
        sn_public_info                          : '29', // параметры настройки сервера, публичные, не секретные

        // ==== 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',
        ns_public_info                          : '1029',

        ns_client_error                         : '1030',
        ns_file_uploaded                        : '1031',

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

        accordance : {
                "navServerInfo/ServerInfo"     : "/serverhelp/#{lang}/serverinfo.html",
                "navServerInfo/OnlineUsers"    : "/serverhelp/#{lang}/infoonlineusers.html",
                "navServerInfo/NssService"     : "/serverhelp/#{lang}/infoadditionalservices.html",
                "navServerInfo/Forum"          : "/serverhelp/#{lang}/infomychattechforum.html",
                "navServerInfo/UserProfile"    : "/serverhelp/#{lang}/userprofile.html",
                "navServerInfo/Terminal"       : "/serverhelp/#{lang}/infomychatterminal.html",

                "navUsers/UserManager"         : "/serverhelp/#{lang}/usermanagement.html",
                "navUsers/UserProfile"         : "/serverhelp/#{lang}/userprofile.html",
                "navUsers/WorkPositions"       : "/serverhelp/#{lang}/userdepartments.html",
                "navUsers/Contacts"            : "/serverhelp/#{lang}/usercommoncontacslist.html",
                "navUsers/UsersImports"        : "/serverhelp/#{lang}/userimportfromfile.html",
                "navUsers/Operators"           : "/serverhelp/#{lang}/usermoderators.html",
                "navUsers/Bans"                : "/serverhelp/#{lang}/userbanlist.html",
                "navUsers/ActiveDirectory"     : "/serverhelp/#{lang}/userimportactivedirectory.html",
                "navUsers/UserGroups"          : "/serverhelp/#{lang}/usersgroups.html",
                "navUsers/AdDomains"           : "/serverhelp/#{lang}/index.html?", // нет, синхронизация

                "navConference/AutoChannels"   : "/serverhelp/#{lang}/conferenceauto.html",
                "navConference/ActiveChannels" : "/serverhelp/#{lang}/conferencesonlinelist.html",

                "navRights/GroupRights"        : "/serverhelp/#{lang}/grouprights.html",
                "navRights/GroupUsers"         : "/serverhelp/#{lang}/grouprightsusers.html",
                "navRights/PluginAccess"       : "/serverhelp/#{lang}/index.html?", // нет

                "navTools/WebSupport"          : "/serverhelp/#{lang}/websupport.html",
                "navTools/MyChatGuest"         : "/serverhelp/#{lang}/mychatguest.html",
                "navTools/History"             : "/serverhelp/#{lang}/logsviewer.html",
                "navTools/BBS"                 : "/serverhelp/#{lang}/index.html?", // нет
                "navTools/Updates"             : "/serverhelp/#{lang}/mychatupdatesystem.html", // +
                "navTools/MCScripts"           : "/serverhelp/#{lang}/mychatscripts.html",
                "navTools/Reboot"              : "/serverhelp/#{lang}/index.html?", // нет
                "navTools/PresetManager"       : "/serverhelp/#{lang}/optionspresetscreate.html", // +
                "navTools/RemoteSettings"      : "/serverhelp/#{lang}/optionspresetsassign.html", // +
                "navTools/Integrations"        : "/serverhelp/#{lang}/toolsintegrations.html", // +
                "navTools/Relocation"          : "/serverhelp/#{lang}/index.html?", // нет
                "navTools/Bots"                : "/serverhelp/#{lang}/index.html?", // нет
                "navTools/TotalClear"          : "/serverhelp/#{lang}/index.html?", // нет
                "navTools/Testport"            : "/serverhelp/#{lang}/toolstestmychatports.html",
                "navTools/CustomMenu"          : "/serverhelp/#{lang}/userscustommenutool.html",

                "navFilters/FilterAntiflood"   : "/serverhelp/#{lang}/filtersantiflood.html",
                "navFilters/FilterBadWords"    : "/serverhelp/#{lang}/filtersbadwords.html",
                "navFilters/FilterIP"          : "/serverhelp/#{lang}/filtersip.html",
                "navFilters/FilterMAC"         : "/serverhelp/#{lang}/filtersmac.html",
                "navFilters/FilterNames"       : "/serverhelp/#{lang}/filtersnames.html",
                "navFilters/FilterHardware"    : "/serverhelp/#{lang}/filtershardwareid.html",

                "navSettings/SettingsNetwork"  : "/serverhelp/#{lang}/networkoptions.html",
                "navSettings/SettingsInfo"     : "/serverhelp/#{lang}/serverinfooptions.html",
                "navSettings/SettingsLogs"     : "/serverhelp/#{lang}/logoptions.html",
                "navSettings/SettingsFTP"      : "/serverhelp/#{lang}/ftpoptions.html",
                "navSettings/SettingsWEB"      : "/serverhelp/#{lang}/webservicesoptions.html",
                "navSettings/SettingsBackup"   : "/serverhelp/#{lang}/backupoptions.html",
                "navSettings/SettingsService"  : "/serverhelp/#{lang}/maintenanceoptions.html",
                "navSettings/SettingsAPI"      : "/serverhelp/#{lang}/integrationapioptions.html",
                "navSettings/SettingsSMTP"     : "/serverhelp/#{lang}/smtpoptions.html",
                "navSettings/SettingsFolders"  : "/serverhelp/#{lang}/mychatserverfolders.html",
                "navSettings/Others"           : "/serverhelp/#{lang}/additionaloptions.html",

                "navLicense/LicenseMyChat"     : "/serverhelp/#{lang}/mychatserverlicenses.html",
                "navLicense/LicenseGuest"      : "/serverhelp/#{lang}/mychatguestlicenses.html",
                "navLicense/LicenseQuiz"       : "/serverhelp/#{lang}/mychatvictorinalicense.html",  // +

                "mychat_versions"              : "/serverhelp/#{lang}/versionshistory.html",

        },

        // ==== 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',
            FILES           : 'files',

            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 // полный трафик от сервера к клиентам и обратно

            // нове найменування
            /// любая ошибка, которая приводит к закрытию службы или приложения для предотвращения потери данных (или дальнейшей
            /// потери данных). Это - самые тяжёлые ошибки и ситуации, когда гарантировано повреждение или потеря данных
            FATAL : 1,
            /// любая ошибка, которая является фатальной для операции, но не для службы или приложения (не удается открыть
            /// требуемый файл, отсутствуют данные, указан неправильный пароль и т.д.). Эти ошибки требуют вмешательства
            /// администратора или пользователя, но не приводят к фатальным сбоям
            ERROR : 2,
            /// все, что потенциально может вызвать странности приложения, но которые программа автоматически устраняет либо
            /// может продолжать работу дальше без фатальных ошибок (например, переключение с основного на резервный сервер,
            /// повторная попытка операции, отсутствие дополнительных данных и т. д.)
            WARN  : 3,
            /// полезная информация для работы. Информация, которую полезно всегда иметь под рукой, но на которую не обращают
            /// внимания в обычных обстоятельствах. Это стандартный уровень протоколирования
            INFO  : 4,
            /// расширенная диагностика (IT персонал, системные администраторы и т.д.)
            DEBUG : 5,
            /// "трассировка", для подробной глубокой отладки, логов будет очень много
            TRACE : 6
        },

        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,                      // "Конференции", "Редактирование своих сообщений"
                
            QKanbanNotifySelfCreateTask: 267,                     // "Канбан", "Уведомления", "Мои задачи", "Создание новой задачи"
            QKanbanNotifySelfChangePerformer: 268,                // "Канбан", "Уведомления", "Мои задачи", "Смена исполнителя"
            QKanbanNotifySelfNewComment: 269,                     // "Канбан", "Уведомления", "Мои задачи", "Новый комментарий"
            QKanbanNotifySelfTaskMove: 270,                       // "Канбан", "Уведомления", "Мои задачи", "Смена этапа"
            QKanbanNotifySelfTaskChange: 271,                     // "Канбан", "Уведомления", "Мои задачи", "Редактирование задачи"
            QKanbanNotifySelfTaskRemove: 272,                     // "Канбан", "Уведомления", "Мои задачи", "Удаление задачи"

            QKanbanNotifyOthersCreateTask: 273,                   // "Канбан", "Уведомления", "Чужие задачи", "Создание новой задачи"
            QKanbanNotifyOthersChangePerformer: 274,              // "Канбан", "Уведомления", "Чужие задачи", "Смена исполнителя"
            QKanbanNotifyOthersNewComment: 275,                   // "Канбан", "Уведомления", "Чужие задачи", "Новый комментарий"
            QKanbanNotifyOthersTaskMove: 276,                     // "Канбан", "Уведомления", "Чужие задачи", "Смена этапа"
            QKanbanNotifyOthersTaskChange: 277,                   // "Канбан", "Уведомления", "Чужие задачи", "Редактирование задачи"
            QKanbanNotifyOthersTaskRemove: 278,                   // "Канбан", "Уведомления", "Чужие задачи", "Удаление задачи"

            QAllowRemoveSelfFreshPrivateMessages: 279,            // "Приваты", "Удаление своих недавно отправленных сообщений"
            QAllowRemoveSelfFreshConfMessages: 280,               // "Конференции", "Удаление своих недавно отправленных сообщений"
            QKanbanAccessToTaskScriptList: 281,                   // "Канбан", "Добавление и удаление событий (скриптов) для задач"

            QSecurityAllowInterfaceScreenShots: 282,              // "Безопасность", "Разрешить скриншоты окна приложения"
            QSecurityAllowChatTextSelect: 283,                    // "Безопасность", "Разрешить выделять текст сообщений для копирования в буфер обмена"

            QPrivAllowSilentMessages: 284,                        // "Приваты", "Разрешить отправку сообщений в "тихом" режиме"
            QTxtChAllowSilentMessages: 285,                       // "Конференции", "Разрешить отправку сообщений в "тихом" режиме"

            QPrivAllowSelfAutoClearHistory: 286,                  // "Приваты", "Разрешить автоматическую локальную очистку истории"
            QPrivAllowAutoClearHistory: 288,                      // "Приваты", "Разрешить автоматическую очистку истории"
            QPrivAllowFullRemoveHistory: 290,                     // "Приваты", "Разрешить полное удаление истории сообщений"

            QTxtChAllowSelfAutoClearHistory: 287,                 // "Конференции", "Разрешить автоматическую локальную очистку истории"
            QTxtChAllowAutoClearHistory: 289,                     // "Конференции", "Разрешить автоматическую очистку истории"
            QTxtChAllowFullRemoveHistory: 291,                    // "Конференции", "Разрешить полное удаление истории сообщений"

            QAllowForwardMessages: 292,                           // "Общие", "Разрешить пересылку сообщений"
            QAllowPIN: 293,                                       // "Общие", "Разрешить PIN-код безопасности"

            QPrivAllowAutoTotalClearHistory: 294,                 // "Приваты", "Разрешить автоматическую очистку истории с удалением на сервере"
            QTxtChAllowAutoTotalClearHistory: 295,                // "Конференции", "Разрешить автоматическую очистку истории с удалением на сервере"
                
            QAdmAccessOptionSecurity: 296,                        // "Доступ к функциям управления сервером", "Настройки", "Безопасность"

            QAllowVoiceMsgPrivates: 297,                          // "Приваты", "Разрешить отправку голосовых сообщений"
            QAllowVoiceMsgConfs: 298,                             // "Конференции", "Разрешить отправку голосовых сообщений"

            QAllowTableMsgPrivates: 299,                          // "Приваты", "Вставка таблиц в сообщениях"
            QAllowTableMsgConfs: 300,                             // "Конференции", "Вставка таблиц в сообщениях"

            QAllowConfReactions: 301,                             // "Конференции", "Разрешить реакции на сообщения"
            QAllowPrivateReactions: 302,                          // "Приваты", "Разрешить реакции на сообщения"

            QAllowGlobalHotkeys: 303,                             // "Общие", "Разрешить глобальные "горячие" клавиши
            QEnableGhostMode: 304,                                // "Общие", "Включить "Ghost mode" (скрытый режим, без иконки в трее)

            QAllowFilesDownload: 305,                             // "Общие", "Разрешить скачивание файлов"
            QAllowScanCodes: 306,                                 // "Общие", "Разрешить сканирование QR/штрих-кодов"

            QAllowMakeUnreadPrivates: 307,                        // "Приваты", "Разрешить ставить отметку "Сделать непрочитанным" в диалогах"
            QAllowMakeUnreadConfs: 308,                           // "Конференции", "Разрешить ставить отметку "Сделать непрочитанным" в диалогах"

        },

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

        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,

            CONF_JOIN           : 54,
            CONF_LEAVE          : 55,

            HISTORY_REMOVED     : 56, // (2024.3+) история сообщений полностью удалена

            REACTION            : 57,

            TABLE               : 58,
            MULTILANG           : 59
        },
        
        msgClientType: {
            WIN32   : 0,
            WEB     : 1,
            MACOS   : 2,
            LINUX   : 3,
            ANDROID : 4
        },

        loginStyle: {
            STANDARD       : '0', // standard login + pwd
            AD             : '1', // active directory login + pwd
            NO_PASS        : '2', // only login
            AD_TRANSPARENT : '3'  // active directory login + ad token
        },

        msgMods: {    // message modifier
            NONE     : 0,
            MODIFIED : 1,
            DELETED  : 2
        },

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

        Errors: { // some errors for context in name
            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",

        denied: '<html><body style="font-family: \'Courier New\', monospace">' +
            'Access denied<br>' +
            'UA: <a target="_blank" href="https://nsoft-s.com/ua/mcserverhelp/mychatclienterrors0279.html">https://nsoft-s.com/ua/mcserverhelp/mychatclienterrors0279.html</a><br>' +
            'RU: <a target="_blank" href="https://nsoft-s.com/mcserverhelp/mychatclienterrors0279.html">https://nsoft-s.com/mcserverhelp/mychatclienterrors0279.html</a><br>' +
            'EN: <a target="_blank" href="https://nsoft-s.com/en/mcserverhelp/mychatclienterrors0279.html">https://nsoft-s.com/en/mcserverhelp/mychatclienterrors0279.html</a>' +
        '</body></html>',
        
        _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: ["websocket", "polling"], //

    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', //

    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=",

    linkType: {
        twitter1: "//twitter.com/",
        twitter2: "//x.com/",
        instagram1: "https://www.instagram.com/",
        instagram2: "https://instagram.com/"
    },

    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,
        "GeneralUITheme"    : "light",
        "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,
        "SysEventsEnableEmotions" : 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,

        "RecordMediaCalls" : 0,
        "RecordMediaCallsDir" : ""
    }
};
(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 mcTerminalEngine(opts) {
    var self = this;

    // --- Поля ---
    this.prefix            = opts.prefix || "";
    this.colorGreen        = opts.colorCursor   || "green";
    this.colorBg           = opts.colorCursorBg || "#3e3e3e";
    this.localCommands     = opts.localCommands     || {};
    this.multiWordPrefixes = opts.multiWordPrefixes || [];
    this._$scope           = opts.$scope;
    this._onUnknownCommand = opts.onUnknownCommand  || function(){};

    this.cmdHistory        = [];
    this.cmdHistoryNumb    = -1;
    this.value             = "";
    this.curPosition       = 0;
    this.curWidth          = 0;

    // DOM-references
    this.inputName    = "__terminal-input";
    this.input        = null;
    this.mirrorName   = "__terminal-input-mirror";
    this.mirror       = null;
    this.terminalName = "__terminal-output-wrapper";
    this.terminal     = null;
    this.wrapperName  = "__terminal-input-wrapper";
    this.wrapper      = null;
    this.cursorName   = "__terminal-cursor";
    this.cursor       = null;

    // --- Методи ---

    this.addTerminalLine = function (text) {
        var pre = document.createElement("pre");

        pre.innerHTML        = text;
        pre.style.marginTop  = "1px";
        pre.style.marginLeft = "8px";

        self.terminal.appendChild(pre);

        self._$scope.terminalWrapper.getNode().scrollTo(0, self.wrapper.offsetTop);
    };

    this.keysHandle = function (code) {
        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 = "1px";

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

                    // Мульти-слівні команди: якщо перше слово є в multiWordPrefixes —
                    // ключем стає весь рядок (наприклад "traffic cmd", "hotkeys debug on")
                    if (mcService.inArrayNoStrict(_cmd, self.multiWordPrefixes) !== -1) {
                        _cmd = mcService.trim(self.value);
                    }

                    if (self.localCommands.hasOwnProperty(_cmd)) {
                        self.localCommands[_cmd].apply(self, params);
                    } else if (_cmd !== "") {
                        self._onUnknownCommand.call(self, _cmd, params);
                    }

                    self._$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.cursor.style.backgroundColor = self.colorGreen;

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

    this.blinkCursor = function () {
        if (self.cursor) {
            setTimeout(function () {
                mcService.hideView(self.cursor);

                setTimeout(function () {
                    mcService.showView(self.cursor);

                    // Не забираємо фокус якщо є активне виділення тексту —
                    // користувач може скопіювати його через Ctrl+C або контекстне меню
                    if (self.input && !window.getSelection().toString()) {
                        self.cursor.style.backgroundColor = self.colorGreen;
                        self.input.focus();
                    } else {
                        self.cursor.style.backgroundColor = self.colorBg;
                    }
                }, 350);
            }, 350);
        }
    };

    this.onfocusInput = function () {
        mcService.setCaretPosition(self.input, self.curPosition);
    };

    this.onblurInput = function () {
        self.curPosition = mcService.getCaretPosition(self.input);
    };

    this.createTerminal = function () {
        self.input    = document.getElementById(self.inputName);
        self.terminal = document.getElementById(self.terminalName);
        self.mirror   = document.getElementById(self.mirrorName);
        self.wrapper  = document.getElementById(self.wrapperName);
        self.cursor   = document.getElementById(self.cursorName);

        self.curWidth = self.cursor.getBoundingClientRect().width;

        self.terminal.onmouseup = function () {
            if (window.getSelection().toString()) {
                self.cursor.style.backgroundColor = self.colorBg;
            }
        };

        // Навмисно: обидва обробники викликають onblurInput —
        // збереження наявної поведінки обох контролерів (не виправляти)
        self.input.onfocus = function () {
            self.onblurInput();
        };

        self.input.onblur = function () {
            self.onblurInput();
        };

        self.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 () {
                self.keysHandle(code);
            }, 10);
        };
    };

    this.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    : self.cursorName,
                prefix    : self.prefix,
                inpName   : self.inputName,
                out       : self.terminalName,
                mirror    : self.mirrorName,
                inpWrapper: self.wrapperName
            }
        );
    };
}

(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;
         }
     }
     var videoCaptureOpt = {
         "minWidth" : 640,
         "minHeight": 480,
         "maxWidth" : 1920,
         "maxHeight": 1080,
         "minAspectRatio": 16/9,
         "maxAspectRatio": 16/9,
         "minFrameRate"  : 24,
         "maxFrameRate"  : 60
     };

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

     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.onIceStateChange = 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();
             };

             calleeContainer = videoContainer;

             Self.changeSpeakerDevice(devices.sound);

             /*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));
         }).catch(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
                 if (calleeUIN) {
                     console.log("Start recall");

                     recall(callee, __pc, true); // 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 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;

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

                 if (Self.onIceStateChange){
                     Self.onIceStateChange(pc.iceConnectionState);
                 }
             };

             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 (Self.onIceStateChange){
             Self.onIceStateChange(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: [
                         // - sampleRate      : INT specifies a desired sample rate, not sure if it should be used as an encoding
                         //                     setting or as a hardware requirement, higher is better (Audio CDs for example
                         //                     have 44000 samples/s or 44kHz)

                         // - sampleSize      : INT each sample’s size in bits, higher is better (Audio CDs have a sample size
                         //                     of 16 bits/sample )

                         // - volume          : FLOAT it takes values between 0.0 (silence) and 1.0 (max volume), it’s used as a
                         //                     multiplier for each sample’s value

                         // - echoCancellation: BOOL whether or not to use echo cancellation to try and remove the audio that went
                         //                     to the speakers from the input that comes through the mic

                         // - autoGainControl : BOOL whether or not to modify the volume of the input from the mic

                         // - noiseSuppression: BOOL whether or not to try and remove the background noise from the audio signal

                         // - latency         : INT specified in seconds, controls the time between the start of the sound processing
                         //                     and the data being made available to the next step, not so sure why you’d
                         //                     want higher latency but audio codecs do differ in latency

                         // - channelCount    : INT specifies the number of channels to use with 1 being mono, 2 being stereo.
                         //                     It works with some webcams and laptops with dual mics.

                         // { 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: videoCaptureOpt,
                         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.loadDevicesList = function (cb) {
         navigator.mediaDevices.enumerateDevices()
         .then(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("loadDevicesList");
             console.log(deviceList);

             cb && cb(deviceList);
         })
         .catch(error => {
             console.error('Error loadDevicesList:', error);
         });
     };

     this.changeMicrophoneDevice = function(newDeviceId) {
         if (myStream) {
             const oldAudioTrack = myStream.getAudioTracks()[0];
             
             if (oldAudioTrack) {

                 navigator.mediaDevices.getUserMedia({
                     audio: { deviceId: { exact: newDeviceId } },
                     video: false
                 }).then(function (newStream) {
                     const newAudioTrack = newStream.getAudioTracks()[0];

                     const senders = pc.getSenders().find(sender => sender.track &&  sender.track.kind === 'audio');

                     senders.replaceTrack(newAudioTrack);

                     setTimeout(function () {
                         oldAudioTrack.stop();

                         myStream.removeTrack(oldAudioTrack);
                         myStream.addTrack(newAudioTrack);
                     }, 300);

                     newAudioTrack.onended = () => {
                         console.log('Новий аудіотрек завершив трансляцію. ' + newAudioTrack.id);
                     };

                     newAudioTrack.onmute = () => {
                         console.log('Новий аудіотрек тимчасово вимкнено. ' + newAudioTrack.id);
                     };

                     newAudioTrack.onunmute = () => {
                         console.log('Новий аудіотрек знову активний. ' + newAudioTrack.id);
                     };
                 }).catch(error => {
                     console.error('Помилка при перемиканні мікрофона:', error);
                 });
             }
         }
     };

     this.changeCameraDevice = function(newDeviceId) {
         if (myStream) {
             const oldVideoTrack = myStream.getVideoTracks()[0];

             if (oldVideoTrack) {
                 navigator.mediaDevices.getUserMedia({
                     video: { deviceId: { exact: newDeviceId } },
                     audio: false
                 }).then(function (newStream) {
                     const newVideoTrack = newStream.getVideoTracks()[0];

                     mirrorContainer.srcObject = newStream;

                     const senders = pc.getSenders().find(sender => sender.track &&  sender.track.kind === 'video');

                     senders.replaceTrack(newVideoTrack);

                     setTimeout(function () {
                         oldVideoTrack.stop();

                         myStream.removeTrack(oldVideoTrack);
                         myStream.addTrack(newVideoTrack);
                     }, 300);

                     oldVideoTrack.onended = () => {
                         console.log('Новий аудіотрек завершив трансляцію. ' + newAudioTrack.id);
                     };

                     oldVideoTrack.onmute = () => {
                         console.log('Новий аудіотрек тимчасово вимкнено. ' + newAudioTrack.id);
                     };

                     oldVideoTrack.onunmute = () => {
                         console.log('Новий аудіотрек знову активний. ' + newAudioTrack.id);
                     };
                 }).catch(error => {
                     console.error('Помилка при перемиканні камери:', error);
                 });
             }
         }
     };

     this.changeSpeakerDevice = function(deviceId) {
         if (!calleeContainer || typeof calleeContainer.setSinkId !== 'function') {
             console.warn("setSinkId not supported in browser");
             
             return false;
         }

         if (deviceId){
             calleeContainer.setSinkId(deviceId).then(() => {
                 console.log(`New speaker: ${deviceId}`);

                 devices.sound = deviceId; // оновимо налаштування поточного звукового пристрою
             }).catch((err) => {
                 console.error(`Error change speaker: ${err.message}`);
             });
         }

         return true;
     };

     this.getMediaDevicesList = function () {
         return deviceList;
     }

     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 (microphone, camera, sound) {
         if (isObject(microphone)){
             devices = {
                 microphone : microphone.microphone,
                 camera     : microphone.camera,
                 sound      : microphone.sound
                 // sound      : "574c823d189c645f65f6bf51105b970b70e32b37dbc14d45e1ad285d52089bba" // microphone.sound
                 // sound      : "514edac8a4f360b1f3afdda7a5f77907e8c2132e2dcb5dc44fde0584433b8978" // microphone.sound
             };
         } else {
             devices = {
                 microphone : microphone,
                 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.setVideoParams = function (opt) {
         if (opt && typeof opt === 'object'){
             videoCaptureOpt["minWidth"]       = opt["minWidth"]       || videoCaptureOpt["minWidth"];
             videoCaptureOpt["minHeight"]      = opt["minHeight"]      || videoCaptureOpt["minHeight"];
             videoCaptureOpt["maxWidth"]       = opt["maxWidth"]       || videoCaptureOpt["maxWidth"];
             videoCaptureOpt["maxHeight"]      = opt["maxHeight"]      || videoCaptureOpt["maxHeight"];
             videoCaptureOpt["minAspectRatio"] = opt["minAspectRatio"] || videoCaptureOpt["minAspectRatio"];
             videoCaptureOpt["maxAspectRatio"] = opt["maxAspectRatio"] || videoCaptureOpt["maxAspectRatio"];
             videoCaptureOpt["minFrameRate"]   = opt["minFrameRate"]   || videoCaptureOpt["minFrameRate"];
             videoCaptureOpt["maxFrameRate"]   = opt["maxFrameRate"]   || videoCaptureOpt["maxFrameRate"];
         }
     };

     this.hangUp        = function (error){
         console.log('Hang Up Call');

         clearTimeout(dialingTimer);
         
         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.
 */



 function CallStates(){
     "use strict";

     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 devicesList = 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();
            }, 10000);
        } 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;
        }
    };

    this.setDevicesList = function (devices) {
        devicesList = devices;
    }
}
// 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 ($rootScope) {
    "use strict";

    var CLIENT_TO_SERVER = 'client-to-server-cmd';
    var SERVER_TO_CLIENT = 'server-to-client-cmd';

    var CR           = "\r";
    var callbackPull = {};
    var pingOptions  =  {
        onError : null,
        onStop  : null,
        onWarn  : null
    };

    window.idMcPingTimer = null;

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

    function _StartPingTimer(_time, opt){
        pingOptions = opt;

        if (mcConst.SessionID) {
            window.idMcPingTimer = setInterval(function(){
                if (mcConst.SessionID) {
                    $rootScope.SendCMDToServer([mcConst._CMD_.Ping, mcConst.SessionID]);
                } else {
                    console.error("mcConst.SessionID is not defined. StopPingTimer()");

                    _StopPingTimer();
                }
            }, _time);
        }

        function RunCMD(CMD_Block, debug) {
            if (debug) {
                console.log("CMD_Block:\n" + CMD_Block);
            }

            if (CMD_Block && CMD_Block.length > 0){
                CMD_Block = mcService.StringToObj(CMD_Block);

                for (var CMD_Numb in CMD_Block){
                    $rootScope.ProcessCMD([CMD_Numb, CMD_Block[CMD_Numb]]);
                }
            }
        }

        _SetCallBackFunctionByCMD(mcConst._CMD_.Ping, function(data){
            if ((data !== "{}") && (data.length > 0)) {
                RunCMD(data);
            }
        });

        return window.idMcPingTimer;
    }

    function _StopPingTimer(err){
        if (window.idMcPingTimer !== null){
            console.warn('PingTimer has been stopped. ' + err);

            clearInterval(window.idMcPingTimer);

            window.idMcPingTimer = null;

            _ClearAllCallBackFunctionByCMD();

            if (err && pingOptions.onError){
                pingOptions.onError(err);
            } else
            if (pingOptions.onStop) {
                pingOptions.onStop();
            }
        }
    }

    function _onError(err) {
        _StopPingTimer();

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

        if (mcConst.socketIO){
            mcConst.socketIO.disconnect();
        }
    }

    function _requestError(reason, arg) {
        console.log("requestError: " + reason + "\narg: " + (arg||null));

        // switch (reason){
        //     case "reconnect"
        // }
        if ((reason === "reconnect" && arg < 6 && pingOptions.onReconnect)) {
            pingOptions.onReconnect();
        } else
        if ((reason === "reconnect" && arg >= 6) ||
             reason === "connect_failed" ||
             reason === "connect_error" ||
             reason === "error" ||
             reason === "disconnect" ||
             reason === "reconnect_error" ||
             reason === "stopPing"){
            _onError(arg);
        }
    }

    function _SendData(_data){
        mcConst.socketIO.emit(CLIENT_TO_SERVER, _data);
    }

    function _RequestSuccess(msg){
        var CR_IDX = msg.length ? msg.indexOf(CR) : -1;
        var CMD    = CR_IDX !== -1 ? msg.slice(0, CR_IDX)                  : "ping";
        var data   = CR_IDX !== -1 ? msg.slice(CR_IDX + 1, msg.length + 1) : msg;

        if (CMD === mcConst._CMD_.SrvError) {
            _onError(data);
        }

        if (callbackPull[CMD]) {
            callbackPull[CMD].callback(data);
        }

        if (callbackPull[CMD] && callbackPull[CMD].needRemove){ // треба перевіряти, чи є ще та команда, бо її могли очистити на колбек
            delete callbackPull[CMD];
        }
    }

    function _SendDataToServer(_data){
        var sendingData = '';
        var prepareData = {};

        if ((!_data) || (_data.length === 0)) {
            if (console && console.warn) {
                console.warn('SendDataToServer: _data is empty or undefined');
            }
        } else {
            switch (typeof _data){
                case "string":
                    sendingData = _data;
                    break;

                case "object":
                    prepareData = mcService.Marge({
                        CMD        : '',
                        Data       : ''
                    }, _data || {});

                    if (prepareData.Data === ''){
                        console.error('SendDataToServer: "Data" is undefined');
                    } else {
                        if (mcService.isObject(prepareData.Data)){
                            prepareData.Data = JSON.stringify(prepareData.Data);
                        }

                        sendingData = prepareData.CMD + CR + prepareData.Data;
                    }
                    break;
            }

            sendingData && _SendData(sendingData);
        }
    }

    function _SetCallBackFunctionByCMD(CMD, callback, needRemove){
        if (callback){
            callbackPull[CMD] = {
                callback   : callback,
                needRemove : needRemove || false
            };
        }
    }

    function _ClearAllCallBackFunctionByCMD(){
        for (var i in callbackPull){
            if (callbackPull.hasOwnProperty(i)) {
                delete callbackPull[i];
            }
        }
    }

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

    this.StartPingTimer   = _StartPingTimer;
    this.StopPingTimer    = _StopPingTimer;
    this.SendDataToServer = _SendDataToServer;
    this.SetCallBackFunctionByCMD      = _SetCallBackFunctionByCMD;
    this.ClearAllCallBackFunctionByCMD = _ClearAllCallBackFunctionByCMD;

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

    mcConst.initWS = function (socketIO) {
        socketIO.on(SERVER_TO_CLIENT, _RequestSuccess);
        socketIO.on('error',          function (err) {
            _requestError('error', err);
        });
        socketIO.on('connect_failed', function (){
            _requestError("connect_failed");
        });
        socketIO.on('connect_error',   function (err) {
            _requestError("connect_error", err);
        });
        socketIO.on('disconnect',   function (reason){
            _requestError("disconnect", reason);
        });
        socketIO.on('reconnect',   function (count) {
            _requestError("reconnect", count);
        });
        socketIO.on('reconnect_error',   function (err) {
            _requestError("reconnect_error", err);
        });
    }
}

/**
 * McHttpUploader - XHR-based file uploader with chunked upload support
 * Replaces Webix uploader with proper error handling (onerror, ontimeout)
 * Files larger than CHUNK_SIZE are split into chunks for reliable upload
 */

function McHttpUploader(options) {
    "use strict";

    var self   = this;
    var xhr    = null;
    var fileId = null;
    var aborted = false;

    var CHUNK_SIZE  = 10 * 1024 * 1024; // 5 MB
    var MAX_RETRIES = 3;

    var uploadUrl  = null;
    var isChunked  = false;

    var progressList      = options.progressListId ? $$(options.progressListId) : null;
    var onAfterFileAdd    = options.onAfterFileAdd    || function(){};
    var onUploadComplete  = options.onUploadComplete  || function(){};
    var onFileUploadError = options.onFileUploadError || function(){};

    function _sendCancel() {
        if (!isChunked || !uploadUrl) return;

        var separator = uploadUrl.indexOf("?") >= 0 ? "&" : "?";
        var cancelXhr = new XMLHttpRequest();

        cancelXhr.open("POST", uploadUrl + separator + "cancel=1", true);
        cancelXhr.send();

        isChunked = false;
        uploadUrl = null;
    }

    function _updateProgressList(id, percent, status) {
        if (progressList && progressList.exists(id)) {
            var item = progressList.getItem(id);

            item.percent = Math.round(percent);
            item.status  = status || item.status;

            progressList.updateItem(id);
        }
    }

    function _uploadWhole(url, file, fileInfo) {
        var formData = new FormData();
        formData.append("upload", file, file.name);

        xhr = new XMLHttpRequest();

        var lastLoaded = 0;

        xhr.upload.addEventListener("progress", function(e) {
            if (e.lengthComputable) {
                lastLoaded = e.loaded;
                var percent = e.loaded / e.total * 100;

                _updateProgressList(fileId, percent, "transfer");
            }
        }, false);

        xhr.onload = function() {
            var response = null;

            try {
                response = JSON.parse(xhr.responseText);
            } catch(e) {}

            if (xhr.status >= 400 || !response || response.status === "error") {
                console.warn("[McHttpUploader] server error, status: " + xhr.status + ", body: " + (xhr.responseText || "").substring(0, 200) + ", sent: " + lastLoaded + "/" + file.size);

                _updateProgressList(fileId, 0, "error");

                onFileUploadError(fileInfo, response);
            } else {
                _updateProgressList(fileId, 100, "server");

                onUploadComplete(fileInfo, response);
            }

            xhr = null;
        };

        xhr.onerror = function() {
            console.warn("[McHttpUploader] XHR onerror, sent: " + lastLoaded + "/" + file.size + ", readyState: " + xhr.readyState + ", status: " + xhr.status);

            _updateProgressList(fileId, 0, "error");

            onFileUploadError(fileInfo, null);

            xhr = null;
        };

        xhr.ontimeout = function() {
            console.warn("[McHttpUploader] XHR timeout, sent: " + lastLoaded + "/" + file.size);

            _updateProgressList(fileId, 0, "error");

            onFileUploadError(fileInfo, null);

            xhr = null;
        };

        xhr.onabort = function() {
            console.warn("[McHttpUploader] XHR aborted, sent: " + lastLoaded + "/" + file.size);

            _updateProgressList(fileId, 0, "error");

            onFileUploadError(fileInfo, null);

            xhr = null;
        };

        xhr.open("POST", url, true);
        xhr.send(formData);
    }

    function _uploadChunked(url, file, fileInfo) {
        var totalChunks  = Math.ceil(file.size / CHUNK_SIZE);
        var currentChunk = 0;
        var totalSent    = 0;
        var retryCount   = 0;
        var chunkHandled = false;
        var separator    = url.indexOf("?") >= 0 ? "&" : "?";

        console.log("[McHttpUploader] chunked upload: " + file.name + ", size: " + file.size + ", chunks: " + totalChunks);

        function onChunkSuccess(response) {
            if (chunkHandled || aborted) return;
            chunkHandled = true;

            retryCount = 0;

            var start = currentChunk * CHUNK_SIZE;
            var end   = Math.min(start + CHUNK_SIZE, file.size);

            totalSent += (end - start);
            currentChunk++;

            var percent = totalSent / file.size * 100;
            _updateProgressList(fileId, percent, "transfer");

            xhr = null;

            if (currentChunk < totalChunks) {
                sendChunk();
            } else {
                console.log("[McHttpUploader] all chunks sent: " + file.name + ", total: " + totalSent);

                _updateProgressList(fileId, 100, "server");

                onUploadComplete(fileInfo, response);
            }
        }

        function onChunkError(reason) {
            if (chunkHandled || aborted) return;
            chunkHandled = true;

            console.warn("[McHttpUploader] chunk " + currentChunk + "/" + totalChunks + " error: " + reason);

            retryCount++;

            if (retryCount <= MAX_RETRIES) {
                console.log("[McHttpUploader] retrying chunk " + currentChunk + ", attempt " + retryCount);

                xhr = null;
                setTimeout(sendChunk, 1000 * retryCount);
            } else {
                _sendCancel();

                _updateProgressList(fileId, 0, "error");

                onFileUploadError(fileInfo, null);

                xhr = null;
            }
        }

        function sendChunk() {
            if (aborted) return;

            chunkHandled = false;

            var start = currentChunk * CHUNK_SIZE;
            var end   = Math.min(start + CHUNK_SIZE, file.size);
            var blob  = file.slice(start, end);

            var chunkUrl = url + separator + "chunk=" + currentChunk + "&chunks=" + totalChunks;

            var formData = new FormData();
            formData.append("upload", blob, file.name);

            xhr = new XMLHttpRequest();

            xhr.onload = function() {
                var response = null;

                try {
                    response = JSON.parse(xhr.responseText);
                } catch(e) {}

                if (xhr.status >= 400 || !response || response.status === "error") {
                    onChunkError("server status " + xhr.status);
                } else {
                    onChunkSuccess(response);
                }
            };

            xhr.onerror = function() {
                onChunkError("XHR onerror, readyState: " + (xhr ? xhr.readyState : "null"));
            };

            xhr.ontimeout = function() {
                onChunkError("XHR timeout");
            };

            xhr.onabort = function() {
                if (!aborted) {
                    onChunkError("XHR aborted");
                }
            };

            xhr.open("POST", chunkUrl, true);
            xhr.send(formData);
        }

        sendChunk();
    }

    this.upload = function(url, file) {
        fileId  = Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
        aborted = false;

        var fileInfo = {
            id      : fileId,
            name    : file.name,
            size    : file.size,
            sizetext: (file.size / (1024 * 1024)).toFixed(2) + " MB",
            percent : 0,
            status  : "transfer"
        };

        if (progressList) {
            progressList.add(fileInfo);
        }

        onAfterFileAdd(fileInfo);

        console.log("[McHttpUploader] starting upload: " + file.name + ", size: " + file.size);

        _updateProgressList(fileId, 0, "transfer");

        if (file.size > CHUNK_SIZE) {
            uploadUrl = url;
            isChunked = true;

            _uploadChunked(url, file, fileInfo);
        } else {
            _uploadWhole(url, file, fileInfo);
        }
    };

    this.stopUpload = function() {
        aborted = true;

        if (xhr) {
            console.warn("[McHttpUploader] stopUpload called, aborting active XHR");
            xhr.abort();
            xhr = null;
        }

        _sendCancel();
    };

    this.destructor = function() {
        if (xhr) {
            console.warn("[McHttpUploader] destructor called with active XHR, trace:", new Error().stack);
        }
        self.stopUpload();

        onAfterFileAdd    = null;
        onUploadComplete  = null;
        onFileUploadError = null;
        progressList      = null;
        xhr               = null;
    };
}
/**
 * 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
            var url = mcService.getLocalHostPath($rootScope.isWebClient) + "/uploading/?sid=" + mcConst.SessionID + "&hash=" + info.Hash + whereWeWork;
            uploader.upload(url, 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; // локальное название файла

        info.MsgType = parseInt(info.MsgType);

        if ((file.type === "image/png" || file.type === "image/jpeg" || file.type === "image/gif") && info.MsgType === mcConst._CMD_.msgType.IMAGE_NEW) {
            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

        // -- clipboard file, only for electron app --
        if (uploaderOptions.filePath && !$rootScope.isWebClient){
            if (uploaderOptions.filePath === true){
                uploaderOptions.filePath = [];

                for (var i = 0; i < uploaderOptions.dropFile.files.length; i++){
                    uploaderOptions.filePath.push(uploaderOptions.dropFile.files[i].path);
                }
            }

            electronUploader();
        } else

        // -- drop file into chat text --
        if (uploaderOptions.filePath && uploaderOptions.dropFile){
            _goUploadFile.apply(uploaderOptions.dropFile);
        } 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) {
            uploader.stopUpload();
            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){
        if (uploader){
            uploader.destructor();
        }

        abortUploadingID = null;
        abortPrepUploading = null;
        abortUploadingCB = null;
        abortNow = false;

        whereWeWork = $rootScope.hasOwnProperty("GetChatType") ? "&where=" + $rootScope.GetChatType().toLowerCase() : "";

        var opt = args[2] || {};
        var progressListId = opt.uploadProgress || "uploadProgress";

        if (!$$(progressListId)){
            webix.ui({ id: progressListId, view: "list", scroll: false, padding: 0, borderless: true, hidden: true, select: false});
        }

        uploader = new McHttpUploader({
            progressListId: progressListId,
            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;
            }
        });

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

             res = new Promise(function (resolve, reject) {
                 var _request = new XMLHttpRequest();
                                                        // true - async request
                 try {
                     _request.open(post ? "POST" : "GET", url, true);

                     _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);
                 } catch (e){
                     console.error(e.message || e);
                 }
             });

             res.success = res.then;

         return res;
     }

     function changeLocation() { // info
         if ($location){
             $location.data = purl().data;

             $rootScope.$broadcast("onChangeHrefPath", [$location], true);
         }
     }

     var units      = [];
     var $location  = null;
     var moduleList = {}; // Loaded Modules
     var eventList  = {}; // Event Emitter
     var $rootScope = {   // Global Scope
         $on         : _on,
         $off        : _off,
         $emit       : _broadcast,
         $broadcast  : _broadcast,
         isWebClient : location.protocol !== "file:"
     };

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


/**
 * Fuse.js v6.6.2 - Lightweight fuzzy-search (http://fusejs.io)
 *
 * Copyright (c) 2022 Kiro Risk (http://kiro.me)
 * All Rights Reserved. Apache Software License 2.0
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 */

(function (global, factory) {
  typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
  typeof define === 'function' && define.amd ? define(factory) :
  (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Fuse = factory());
})(this, (function () { 'use strict';

  function ownKeys(object, enumerableOnly) {
    var keys = Object.keys(object);

    if (Object.getOwnPropertySymbols) {
      var symbols = Object.getOwnPropertySymbols(object);
      enumerableOnly && (symbols = symbols.filter(function (sym) {
        return Object.getOwnPropertyDescriptor(object, sym).enumerable;
      })), keys.push.apply(keys, symbols);
    }

    return keys;
  }

  function _objectSpread2(target) {
    for (var i = 1; i < arguments.length; i++) {
      var source = null != arguments[i] ? arguments[i] : {};
      i % 2 ? ownKeys(Object(source), !0).forEach(function (key) {
        _defineProperty(target, key, source[key]);
      }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(target, Object.getOwnPropertyDescriptors(source)) : ownKeys(Object(source)).forEach(function (key) {
        Object.defineProperty(target, key, Object.getOwnPropertyDescriptor(source, key));
      });
    }

    return target;
  }

  function _typeof(obj) {
    "@babel/helpers - typeof";

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

  function _classCallCheck(instance, Constructor) {
    if (!(instance instanceof Constructor)) {
      throw new TypeError("Cannot call a class as a 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);
    }
  }

  function _createClass(Constructor, protoProps, staticProps) {
    if (protoProps) _defineProperties(Constructor.prototype, protoProps);
    if (staticProps) _defineProperties(Constructor, staticProps);
    Object.defineProperty(Constructor, "prototype", {
      writable: false
    });
    return Constructor;
  }

  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 _inherits(subClass, superClass) {
    if (typeof superClass !== "function" && superClass !== null) {
      throw new TypeError("Super expression must either be null or a function");
    }

    Object.defineProperty(subClass, "prototype", {
      value: Object.create(superClass && superClass.prototype, {
        constructor: {
          value: subClass,
          writable: true,
          configurable: true
        }
      }),
      writable: false
    });
    if (superClass) _setPrototypeOf(subClass, superClass);
  }

  function _getPrototypeOf(o) {
    _getPrototypeOf = Object.setPrototypeOf ? Object.getPrototypeOf : function _getPrototypeOf(o) {
      return o.__proto__ || Object.getPrototypeOf(o);
    };
    return _getPrototypeOf(o);
  }

  function _setPrototypeOf(o, p) {
    _setPrototypeOf = Object.setPrototypeOf || function _setPrototypeOf(o, p) {
      o.__proto__ = p;
      return o;
    };

    return _setPrototypeOf(o, p);
  }

  function _isNativeReflectConstruct() {
    if (typeof Reflect === "undefined" || !Reflect.construct) return false;
    if (Reflect.construct.sham) return false;
    if (typeof Proxy === "function") return true;

    try {
      Boolean.prototype.valueOf.call(Reflect.construct(Boolean, [], function () {}));
      return true;
    } catch (e) {
      return false;
    }
  }

  function _assertThisInitialized(self) {
    if (self === void 0) {
      throw new ReferenceError("this hasn't been initialised - super() hasn't been called");
    }

    return self;
  }

  function _possibleConstructorReturn(self, call) {
    if (call && (typeof call === "object" || typeof call === "function")) {
      return call;
    } else if (call !== void 0) {
      throw new TypeError("Derived constructors may only return object or undefined");
    }

    return _assertThisInitialized(self);
  }

  function _createSuper(Derived) {
    var hasNativeReflectConstruct = _isNativeReflectConstruct();

    return function _createSuperInternal() {
      var Super = _getPrototypeOf(Derived),
          result;

      if (hasNativeReflectConstruct) {
        var NewTarget = _getPrototypeOf(this).constructor;

        result = Reflect.construct(Super, arguments, NewTarget);
      } else {
        result = Super.apply(this, arguments);
      }

      return _possibleConstructorReturn(this, result);
    };
  }

  function _toConsumableArray(arr) {
    return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _unsupportedIterableToArray(arr) || _nonIterableSpread();
  }

  function _arrayWithoutHoles(arr) {
    if (Array.isArray(arr)) return _arrayLikeToArray(arr);
  }

  function _iterableToArray(iter) {
    if (typeof Symbol !== "undefined" && iter[Symbol.iterator] != null || iter["@@iterator"] != null) return Array.from(iter);
  }

  function _unsupportedIterableToArray(o, minLen) {
    if (!o) return;
    if (typeof o === "string") return _arrayLikeToArray(o, minLen);
    var n = Object.prototype.toString.call(o).slice(8, -1);
    if (n === "Object" && o.constructor) n = o.constructor.name;
    if (n === "Map" || n === "Set") return Array.from(o);
    if (n === "Arguments" || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(n)) return _arrayLikeToArray(o, minLen);
  }

  function _arrayLikeToArray(arr, len) {
    if (len == null || len > arr.length) len = arr.length;

    for (var i = 0, arr2 = new Array(len); i < len; i++) arr2[i] = arr[i];

    return arr2;
  }

  function _nonIterableSpread() {
    throw new TypeError("Invalid attempt to spread non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
  }

  function isArray(value) {
    return !Array.isArray ? getTag(value) === '[object Array]' : Array.isArray(value);
  } // Adapted from: https://github.com/lodash/lodash/blob/master/.internal/baseToString.js

  var INFINITY = 1 / 0;
  function baseToString(value) {
    // Exit early for strings to avoid a performance hit in some environments.
    if (typeof value == 'string') {
      return value;
    }

    var result = value + '';
    return result == '0' && 1 / value == -INFINITY ? '-0' : result;
  }
  function toString(value) {
    return value == null ? '' : baseToString(value);
  }
  function isString(value) {
    return typeof value === 'string';
  }
  function isNumber(value) {
    return typeof value === 'number';
  } // Adapted from: https://github.com/lodash/lodash/blob/master/isBoolean.js

  function isBoolean(value) {
    return value === true || value === false || isObjectLike(value) && getTag(value) == '[object Boolean]';
  }
  function isObject(value) {
    return _typeof(value) === 'object';
  } // Checks if `value` is object-like.

  function isObjectLike(value) {
    return isObject(value) && value !== null;
  }
  function isDefined(value) {
    return value !== undefined && value !== null;
  }
  function isBlank(value) {
    return !value.trim().length;
  } // Gets the `toStringTag` of `value`.
  // Adapted from: https://github.com/lodash/lodash/blob/master/.internal/getTag.js

  function getTag(value) {
    return value == null ? value === undefined ? '[object Undefined]' : '[object Null]' : Object.prototype.toString.call(value);
  }

  var EXTENDED_SEARCH_UNAVAILABLE = 'Extended search is not available';
  var INCORRECT_INDEX_TYPE = "Incorrect 'index' type";
  var LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY = function LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key) {
    return "Invalid value for key ".concat(key);
  };
  var PATTERN_LENGTH_TOO_LARGE = function PATTERN_LENGTH_TOO_LARGE(max) {
    return "Pattern length exceeds max of ".concat(max, ".");
  };
  var MISSING_KEY_PROPERTY = function MISSING_KEY_PROPERTY(name) {
    return "Missing ".concat(name, " property in key");
  };
  var INVALID_KEY_WEIGHT_VALUE = function INVALID_KEY_WEIGHT_VALUE(key) {
    return "Property 'weight' in key '".concat(key, "' must be a positive integer");
  };

  var hasOwn = Object.prototype.hasOwnProperty;

  var KeyStore = /*#__PURE__*/function () {
    function KeyStore(keys) {
      var _this = this;

      _classCallCheck(this, KeyStore);

      this._keys = [];
      this._keyMap = {};
      var totalWeight = 0;
      keys.forEach(function (key) {
        var obj = createKey(key);
        totalWeight += obj.weight;

        _this._keys.push(obj);

        _this._keyMap[obj.id] = obj;
        totalWeight += obj.weight;
      }); // Normalize weights so that their sum is equal to 1

      this._keys.forEach(function (key) {
        key.weight /= totalWeight;
      });
    }

    _createClass(KeyStore, [{
      key: "get",
      value: function get(keyId) {
        return this._keyMap[keyId];
      }
    }, {
      key: "keys",
      value: function keys() {
        return this._keys;
      }
    }, {
      key: "toJSON",
      value: function toJSON() {
        return JSON.stringify(this._keys);
      }
    }]);

    return KeyStore;
  }();
  function createKey(key) {
    var path = null;
    var id = null;
    var src = null;
    var weight = 1;
    var getFn = null;

    if (isString(key) || isArray(key)) {
      src = key;
      path = createKeyPath(key);
      id = createKeyId(key);
    } else {
      if (!hasOwn.call(key, 'name')) {
        throw new Error(MISSING_KEY_PROPERTY('name'));
      }

      var name = key.name;
      src = name;

      if (hasOwn.call(key, 'weight')) {
        weight = key.weight;

        if (weight <= 0) {
          throw new Error(INVALID_KEY_WEIGHT_VALUE(name));
        }
      }

      path = createKeyPath(name);
      id = createKeyId(name);
      getFn = key.getFn;
    }

    return {
      path: path,
      id: id,
      weight: weight,
      src: src,
      getFn: getFn
    };
  }
  function createKeyPath(key) {
    return isArray(key) ? key : key.split('.');
  }
  function createKeyId(key) {
    return isArray(key) ? key.join('.') : key;
  }

  function get(obj, path) {
    var list = [];
    var arr = false;

    var deepGet = function deepGet(obj, path, index) {
      if (!isDefined(obj)) {
        return;
      }

      if (!path[index]) {
        // If there's no path left, we've arrived at the object we care about.
        list.push(obj);
      } else {
        var key = path[index];
        var value = obj[key];

        if (!isDefined(value)) {
          return;
        } // If we're at the last value in the path, and if it's a string/number/bool,
        // add it to the list


        if (index === path.length - 1 && (isString(value) || isNumber(value) || isBoolean(value))) {
          list.push(toString(value));
        } else if (isArray(value)) {
          arr = true; // Search each item in the array.

          for (var i = 0, len = value.length; i < len; i += 1) {
            deepGet(value[i], path, index + 1);
          }
        } else if (path.length) {
          // An object. Recurse further.
          deepGet(value, path, index + 1);
        }
      }
    }; // Backwards compatibility (since path used to be a string)


    deepGet(obj, isString(path) ? path.split('.') : path, 0);
    return arr ? list : list[0];
  }

  var MatchOptions = {
    // Whether the matches should be included in the result set. When `true`, each record in the result
    // set will include the indices of the matched characters.
    // These can consequently be used for highlighting purposes.
    includeMatches: false,
    // When `true`, the matching function will continue to the end of a search pattern even if
    // a perfect match has already been located in the string.
    findAllMatches: false,
    // Minimum number of characters that must be matched before a result is considered a match
    minMatchCharLength: 1
  };
  var BasicOptions = {
    // When `true`, the algorithm continues searching to the end of the input even if a perfect
    // match is found before the end of the same input.
    isCaseSensitive: false,
    // When true, the matching function will continue to the end of a search pattern even if
    includeScore: false,
    // List of properties that will be searched. This also supports nested properties.
    keys: [],
    // Whether to sort the result list, by score
    shouldSort: true,
    // Default sort function: sort by ascending score, ascending index
    sortFn: function sortFn(a, b) {
      return a.score === b.score ? a.idx < b.idx ? -1 : 1 : a.score < b.score ? -1 : 1;
    }
  };
  var FuzzyOptions = {
    // Approximately where in the text is the pattern expected to be found?
    location: 0,
    // At what point does the match algorithm give up. A threshold of '0.0' requires a perfect match
    // (of both letters and location), a threshold of '1.0' would match anything.
    threshold: 0.6,
    // Determines how close the match must be to the fuzzy location (specified above).
    // An exact letter match which is 'distance' characters away from the fuzzy location
    // would score as a complete mismatch. A distance of '0' requires the match be at
    // the exact location specified, a threshold of '1000' would require a perfect match
    // to be within 800 characters of the fuzzy location to be found using a 0.8 threshold.
    distance: 100
  };
  var AdvancedOptions = {
    // When `true`, it enables the use of unix-like search commands
    useExtendedSearch: false,
    // The get function to use when fetching an object's properties.
    // The default will search nested paths *ie foo.bar.baz*
    getFn: get,
    // When `true`, search will ignore `location` and `distance`, so it won't matter
    // where in the string the pattern appears.
    // More info: https://fusejs.io/concepts/scoring-theory.html#fuzziness-score
    ignoreLocation: false,
    // When `true`, the calculation for the relevance score (used for sorting) will
    // ignore the field-length norm.
    // More info: https://fusejs.io/concepts/scoring-theory.html#field-length-norm
    ignoreFieldNorm: false,
    // The weight to determine how much field length norm effects scoring.
    fieldNormWeight: 1
  };
  var Config = _objectSpread2(_objectSpread2(_objectSpread2(_objectSpread2({}, BasicOptions), MatchOptions), FuzzyOptions), AdvancedOptions);

  var SPACE = /[^ ]+/g; // Field-length norm: the shorter the field, the higher the weight.
  // Set to 3 decimals to reduce index size.

  function norm() {
    var weight = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : 1;
    var mantissa = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 3;
    var cache = new Map();
    var m = Math.pow(10, mantissa);
    return {
      get: function get(value) {
        var numTokens = value.match(SPACE).length;

        if (cache.has(numTokens)) {
          return cache.get(numTokens);
        } // Default function is 1/sqrt(x), weight makes that variable


        var norm = 1 / Math.pow(numTokens, 0.5 * weight); // In place of `toFixed(mantissa)`, for faster computation

        var n = parseFloat(Math.round(norm * m) / m);
        cache.set(numTokens, n);
        return n;
      },
      clear: function clear() {
        cache.clear();
      }
    };
  }

  var FuseIndex = /*#__PURE__*/function () {
    function FuseIndex() {
      var _ref = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
          _ref$getFn = _ref.getFn,
          getFn = _ref$getFn === void 0 ? Config.getFn : _ref$getFn,
          _ref$fieldNormWeight = _ref.fieldNormWeight,
          fieldNormWeight = _ref$fieldNormWeight === void 0 ? Config.fieldNormWeight : _ref$fieldNormWeight;

      _classCallCheck(this, FuseIndex);

      this.norm = norm(fieldNormWeight, 3);
      this.getFn = getFn;
      this.isCreated = false;
      this.setIndexRecords();
    }

    _createClass(FuseIndex, [{
      key: "setSources",
      value: function setSources() {
        var docs = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
        this.docs = docs;
      }
    }, {
      key: "setIndexRecords",
      value: function setIndexRecords() {
        var records = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
        this.records = records;
      }
    }, {
      key: "setKeys",
      value: function setKeys() {
        var _this = this;

        var keys = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
        this.keys = keys;
        this._keysMap = {};
        keys.forEach(function (key, idx) {
          _this._keysMap[key.id] = idx;
        });
      }
    }, {
      key: "create",
      value: function create() {
        var _this2 = this;

        if (this.isCreated || !this.docs.length) {
          return;
        }

        this.isCreated = true; // List is Array<String>

        if (isString(this.docs[0])) {
          this.docs.forEach(function (doc, docIndex) {
            _this2._addString(doc, docIndex);
          });
        } else {
          // List is Array<Object>
          this.docs.forEach(function (doc, docIndex) {
            _this2._addObject(doc, docIndex);
          });
        }

        this.norm.clear();
      } // Adds a doc to the end of the index

    }, {
      key: "add",
      value: function add(doc) {
        var idx = this.size();

        if (isString(doc)) {
          this._addString(doc, idx);
        } else {
          this._addObject(doc, idx);
        }
      } // Removes the doc at the specified index of the index

    }, {
      key: "removeAt",
      value: function removeAt(idx) {
        this.records.splice(idx, 1); // Change ref index of every subsquent doc

        for (var i = idx, len = this.size(); i < len; i += 1) {
          this.records[i].i -= 1;
        }
      }
    }, {
      key: "getValueForItemAtKeyId",
      value: function getValueForItemAtKeyId(item, keyId) {
        return item[this._keysMap[keyId]];
      }
    }, {
      key: "size",
      value: function size() {
        return this.records.length;
      }
    }, {
      key: "_addString",
      value: function _addString(doc, docIndex) {
        if (!isDefined(doc) || isBlank(doc)) {
          return;
        }

        var record = {
          v: doc,
          i: docIndex,
          n: this.norm.get(doc)
        };
        this.records.push(record);
      }
    }, {
      key: "_addObject",
      value: function _addObject(doc, docIndex) {
        var _this3 = this;

        var record = {
          i: docIndex,
          $: {}
        }; // Iterate over every key (i.e, path), and fetch the value at that key

        this.keys.forEach(function (key, keyIndex) {
          var value = key.getFn ? key.getFn(doc) : _this3.getFn(doc, key.path);

          if (!isDefined(value)) {
            return;
          }

          if (isArray(value)) {
            (function () {
              var subRecords = [];
              var stack = [{
                nestedArrIndex: -1,
                value: value
              }];

              while (stack.length) {
                var _stack$pop = stack.pop(),
                    nestedArrIndex = _stack$pop.nestedArrIndex,
                    _value = _stack$pop.value;

                if (!isDefined(_value)) {
                  continue;
                }

                if (isString(_value) && !isBlank(_value)) {
                  var subRecord = {
                    v: _value,
                    i: nestedArrIndex,
                    n: _this3.norm.get(_value)
                  };
                  subRecords.push(subRecord);
                } else if (isArray(_value)) {
                  _value.forEach(function (item, k) {
                    stack.push({
                      nestedArrIndex: k,
                      value: item
                    });
                  });
                } else ;
              }

              record.$[keyIndex] = subRecords;
            })();
          } else if (isString(value) && !isBlank(value)) {
            var subRecord = {
              v: value,
              n: _this3.norm.get(value)
            };
            record.$[keyIndex] = subRecord;
          }
        });
        this.records.push(record);
      }
    }, {
      key: "toJSON",
      value: function toJSON() {
        return {
          keys: this.keys,
          records: this.records
        };
      }
    }]);

    return FuseIndex;
  }();
  function createIndex(keys, docs) {
    var _ref2 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
        _ref2$getFn = _ref2.getFn,
        getFn = _ref2$getFn === void 0 ? Config.getFn : _ref2$getFn,
        _ref2$fieldNormWeight = _ref2.fieldNormWeight,
        fieldNormWeight = _ref2$fieldNormWeight === void 0 ? Config.fieldNormWeight : _ref2$fieldNormWeight;

    var myIndex = new FuseIndex({
      getFn: getFn,
      fieldNormWeight: fieldNormWeight
    });
    myIndex.setKeys(keys.map(createKey));
    myIndex.setSources(docs);
    myIndex.create();
    return myIndex;
  }
  function parseIndex(data) {
    var _ref3 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
        _ref3$getFn = _ref3.getFn,
        getFn = _ref3$getFn === void 0 ? Config.getFn : _ref3$getFn,
        _ref3$fieldNormWeight = _ref3.fieldNormWeight,
        fieldNormWeight = _ref3$fieldNormWeight === void 0 ? Config.fieldNormWeight : _ref3$fieldNormWeight;

    var keys = data.keys,
        records = data.records;
    var myIndex = new FuseIndex({
      getFn: getFn,
      fieldNormWeight: fieldNormWeight
    });
    myIndex.setKeys(keys);
    myIndex.setIndexRecords(records);
    return myIndex;
  }

  function computeScore$1(pattern) {
    var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
        _ref$errors = _ref.errors,
        errors = _ref$errors === void 0 ? 0 : _ref$errors,
        _ref$currentLocation = _ref.currentLocation,
        currentLocation = _ref$currentLocation === void 0 ? 0 : _ref$currentLocation,
        _ref$expectedLocation = _ref.expectedLocation,
        expectedLocation = _ref$expectedLocation === void 0 ? 0 : _ref$expectedLocation,
        _ref$distance = _ref.distance,
        distance = _ref$distance === void 0 ? Config.distance : _ref$distance,
        _ref$ignoreLocation = _ref.ignoreLocation,
        ignoreLocation = _ref$ignoreLocation === void 0 ? Config.ignoreLocation : _ref$ignoreLocation;

    var accuracy = errors / pattern.length;

    if (ignoreLocation) {
      return accuracy;
    }

    var proximity = Math.abs(expectedLocation - currentLocation);

    if (!distance) {
      // Dodge divide by zero error.
      return proximity ? 1.0 : accuracy;
    }

    return accuracy + proximity / distance;
  }

  function convertMaskToIndices() {
    var matchmask = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
    var minMatchCharLength = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : Config.minMatchCharLength;
    var indices = [];
    var start = -1;
    var end = -1;
    var i = 0;

    for (var len = matchmask.length; i < len; i += 1) {
      var match = matchmask[i];

      if (match && start === -1) {
        start = i;
      } else if (!match && start !== -1) {
        end = i - 1;

        if (end - start + 1 >= minMatchCharLength) {
          indices.push([start, end]);
        }

        start = -1;
      }
    } // (i-1 - start) + 1 => i - start


    if (matchmask[i - 1] && i - start >= minMatchCharLength) {
      indices.push([start, i - 1]);
    }

    return indices;
  }

  // Machine word size
  var MAX_BITS = 32;

  function search(text, pattern, patternAlphabet) {
    var _ref = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {},
        _ref$location = _ref.location,
        location = _ref$location === void 0 ? Config.location : _ref$location,
        _ref$distance = _ref.distance,
        distance = _ref$distance === void 0 ? Config.distance : _ref$distance,
        _ref$threshold = _ref.threshold,
        threshold = _ref$threshold === void 0 ? Config.threshold : _ref$threshold,
        _ref$findAllMatches = _ref.findAllMatches,
        findAllMatches = _ref$findAllMatches === void 0 ? Config.findAllMatches : _ref$findAllMatches,
        _ref$minMatchCharLeng = _ref.minMatchCharLength,
        minMatchCharLength = _ref$minMatchCharLeng === void 0 ? Config.minMatchCharLength : _ref$minMatchCharLeng,
        _ref$includeMatches = _ref.includeMatches,
        includeMatches = _ref$includeMatches === void 0 ? Config.includeMatches : _ref$includeMatches,
        _ref$ignoreLocation = _ref.ignoreLocation,
        ignoreLocation = _ref$ignoreLocation === void 0 ? Config.ignoreLocation : _ref$ignoreLocation;

    if (pattern.length > MAX_BITS) {
      throw new Error(PATTERN_LENGTH_TOO_LARGE(MAX_BITS));
    }

    var patternLen = pattern.length; // Set starting location at beginning text and initialize the alphabet.

    var textLen = text.length; // Handle the case when location > text.length

    var expectedLocation = Math.max(0, Math.min(location, textLen)); // Highest score beyond which we give up.

    var currentThreshold = threshold; // Is there a nearby exact match? (speedup)

    var bestLocation = expectedLocation; // Performance: only computer matches when the minMatchCharLength > 1
    // OR if `includeMatches` is true.

    var computeMatches = minMatchCharLength > 1 || includeMatches; // A mask of the matches, used for building the indices

    var matchMask = computeMatches ? Array(textLen) : [];
    var index; // Get all exact matches, here for speed up

    while ((index = text.indexOf(pattern, bestLocation)) > -1) {
      var score = computeScore$1(pattern, {
        currentLocation: index,
        expectedLocation: expectedLocation,
        distance: distance,
        ignoreLocation: ignoreLocation
      });
      currentThreshold = Math.min(score, currentThreshold);
      bestLocation = index + patternLen;

      if (computeMatches) {
        var i = 0;

        while (i < patternLen) {
          matchMask[index + i] = 1;
          i += 1;
        }
      }
    } // Reset the best location


    bestLocation = -1;
    var lastBitArr = [];
    var finalScore = 1;
    var binMax = patternLen + textLen;
    var mask = 1 << patternLen - 1;

    for (var _i = 0; _i < patternLen; _i += 1) {
      // Scan for the best match; each iteration allows for one more error.
      // Run a binary search to determine how far from the match location we can stray
      // at this error level.
      var binMin = 0;
      var binMid = binMax;

      while (binMin < binMid) {
        var _score2 = computeScore$1(pattern, {
          errors: _i,
          currentLocation: expectedLocation + binMid,
          expectedLocation: expectedLocation,
          distance: distance,
          ignoreLocation: ignoreLocation
        });

        if (_score2 <= currentThreshold) {
          binMin = binMid;
        } else {
          binMax = binMid;
        }

        binMid = Math.floor((binMax - binMin) / 2 + binMin);
      } // Use the result from this iteration as the maximum for the next.


      binMax = binMid;
      var start = Math.max(1, expectedLocation - binMid + 1);
      var finish = findAllMatches ? textLen : Math.min(expectedLocation + binMid, textLen) + patternLen; // Initialize the bit array

      var bitArr = Array(finish + 2);
      bitArr[finish + 1] = (1 << _i) - 1;

      for (var j = finish; j >= start; j -= 1) {
        var currentLocation = j - 1;
        var charMatch = patternAlphabet[text.charAt(currentLocation)];

        if (computeMatches) {
          // Speed up: quick bool to int conversion (i.e, `charMatch ? 1 : 0`)
          matchMask[currentLocation] = +!!charMatch;
        } // First pass: exact match


        bitArr[j] = (bitArr[j + 1] << 1 | 1) & charMatch; // Subsequent passes: fuzzy match

        if (_i) {
          bitArr[j] |= (lastBitArr[j + 1] | lastBitArr[j]) << 1 | 1 | lastBitArr[j + 1];
        }

        if (bitArr[j] & mask) {
          finalScore = computeScore$1(pattern, {
            errors: _i,
            currentLocation: currentLocation,
            expectedLocation: expectedLocation,
            distance: distance,
            ignoreLocation: ignoreLocation
          }); // This match will almost certainly be better than any existing match.
          // But check anyway.

          if (finalScore <= currentThreshold) {
            // Indeed it is
            currentThreshold = finalScore;
            bestLocation = currentLocation; // Already passed `loc`, downhill from here on in.

            if (bestLocation <= expectedLocation) {
              break;
            } // When passing `bestLocation`, don't exceed our current distance from `expectedLocation`.


            start = Math.max(1, 2 * expectedLocation - bestLocation);
          }
        }
      } // No hope for a (better) match at greater error levels.


      var _score = computeScore$1(pattern, {
        errors: _i + 1,
        currentLocation: expectedLocation,
        expectedLocation: expectedLocation,
        distance: distance,
        ignoreLocation: ignoreLocation
      });

      if (_score > currentThreshold) {
        break;
      }

      lastBitArr = bitArr;
    }

    var result = {
      isMatch: bestLocation >= 0,
      // Count exact matches (those with a score of 0) to be "almost" exact
      score: Math.max(0.001, finalScore)
    };

    if (computeMatches) {
      var indices = convertMaskToIndices(matchMask, minMatchCharLength);

      if (!indices.length) {
        result.isMatch = false;
      } else if (includeMatches) {
        result.indices = indices;
      }
    }

    return result;
  }

  function createPatternAlphabet(pattern) {
    var mask = {};

    for (var i = 0, len = pattern.length; i < len; i += 1) {
      var _char = pattern.charAt(i);

      mask[_char] = (mask[_char] || 0) | 1 << len - i - 1;
    }

    return mask;
  }

  var BitapSearch = /*#__PURE__*/function () {
    function BitapSearch(pattern) {
      var _this = this;

      var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
          _ref$location = _ref.location,
          location = _ref$location === void 0 ? Config.location : _ref$location,
          _ref$threshold = _ref.threshold,
          threshold = _ref$threshold === void 0 ? Config.threshold : _ref$threshold,
          _ref$distance = _ref.distance,
          distance = _ref$distance === void 0 ? Config.distance : _ref$distance,
          _ref$includeMatches = _ref.includeMatches,
          includeMatches = _ref$includeMatches === void 0 ? Config.includeMatches : _ref$includeMatches,
          _ref$findAllMatches = _ref.findAllMatches,
          findAllMatches = _ref$findAllMatches === void 0 ? Config.findAllMatches : _ref$findAllMatches,
          _ref$minMatchCharLeng = _ref.minMatchCharLength,
          minMatchCharLength = _ref$minMatchCharLeng === void 0 ? Config.minMatchCharLength : _ref$minMatchCharLeng,
          _ref$isCaseSensitive = _ref.isCaseSensitive,
          isCaseSensitive = _ref$isCaseSensitive === void 0 ? Config.isCaseSensitive : _ref$isCaseSensitive,
          _ref$ignoreLocation = _ref.ignoreLocation,
          ignoreLocation = _ref$ignoreLocation === void 0 ? Config.ignoreLocation : _ref$ignoreLocation;

      _classCallCheck(this, BitapSearch);

      this.options = {
        location: location,
        threshold: threshold,
        distance: distance,
        includeMatches: includeMatches,
        findAllMatches: findAllMatches,
        minMatchCharLength: minMatchCharLength,
        isCaseSensitive: isCaseSensitive,
        ignoreLocation: ignoreLocation
      };
      this.pattern = isCaseSensitive ? pattern : pattern.toLowerCase();
      this.chunks = [];

      if (!this.pattern.length) {
        return;
      }

      var addChunk = function addChunk(pattern, startIndex) {
        _this.chunks.push({
          pattern: pattern,
          alphabet: createPatternAlphabet(pattern),
          startIndex: startIndex
        });
      };

      var len = this.pattern.length;

      if (len > MAX_BITS) {
        var i = 0;
        var remainder = len % MAX_BITS;
        var end = len - remainder;

        while (i < end) {
          addChunk(this.pattern.substr(i, MAX_BITS), i);
          i += MAX_BITS;
        }

        if (remainder) {
          var startIndex = len - MAX_BITS;
          addChunk(this.pattern.substr(startIndex), startIndex);
        }
      } else {
        addChunk(this.pattern, 0);
      }
    }

    _createClass(BitapSearch, [{
      key: "searchIn",
      value: function searchIn(text) {
        var _this$options = this.options,
            isCaseSensitive = _this$options.isCaseSensitive,
            includeMatches = _this$options.includeMatches;

        if (!isCaseSensitive) {
          text = text.toLowerCase();
        } // Exact match


        if (this.pattern === text) {
          var _result = {
            isMatch: true,
            score: 0
          };

          if (includeMatches) {
            _result.indices = [[0, text.length - 1]];
          }

          return _result;
        } // Otherwise, use Bitap algorithm


        var _this$options2 = this.options,
            location = _this$options2.location,
            distance = _this$options2.distance,
            threshold = _this$options2.threshold,
            findAllMatches = _this$options2.findAllMatches,
            minMatchCharLength = _this$options2.minMatchCharLength,
            ignoreLocation = _this$options2.ignoreLocation;
        var allIndices = [];
        var totalScore = 0;
        var hasMatches = false;
        this.chunks.forEach(function (_ref2) {
          var pattern = _ref2.pattern,
              alphabet = _ref2.alphabet,
              startIndex = _ref2.startIndex;

          var _search = search(text, pattern, alphabet, {
            location: location + startIndex,
            distance: distance,
            threshold: threshold,
            findAllMatches: findAllMatches,
            minMatchCharLength: minMatchCharLength,
            includeMatches: includeMatches,
            ignoreLocation: ignoreLocation
          }),
              isMatch = _search.isMatch,
              score = _search.score,
              indices = _search.indices;

          if (isMatch) {
            hasMatches = true;
          }

          totalScore += score;

          if (isMatch && indices) {
            allIndices = [].concat(_toConsumableArray(allIndices), _toConsumableArray(indices));
          }
        });
        var result = {
          isMatch: hasMatches,
          score: hasMatches ? totalScore / this.chunks.length : 1
        };

        if (hasMatches && includeMatches) {
          result.indices = allIndices;
        }

        return result;
      }
    }]);

    return BitapSearch;
  }();

  var BaseMatch = /*#__PURE__*/function () {
    function BaseMatch(pattern) {
      _classCallCheck(this, BaseMatch);

      this.pattern = pattern;
    }

    _createClass(BaseMatch, [{
      key: "search",
      value: function
        /*text*/
      search() {}
    }], [{
      key: "isMultiMatch",
      value: function isMultiMatch(pattern) {
        return getMatch(pattern, this.multiRegex);
      }
    }, {
      key: "isSingleMatch",
      value: function isSingleMatch(pattern) {
        return getMatch(pattern, this.singleRegex);
      }
    }]);

    return BaseMatch;
  }();

  function getMatch(pattern, exp) {
    var matches = pattern.match(exp);
    return matches ? matches[1] : null;
  }

  var ExactMatch = /*#__PURE__*/function (_BaseMatch) {
    _inherits(ExactMatch, _BaseMatch);

    var _super = _createSuper(ExactMatch);

    function ExactMatch(pattern) {
      _classCallCheck(this, ExactMatch);

      return _super.call(this, pattern);
    }

    _createClass(ExactMatch, [{
      key: "search",
      value: function search(text) {
        var isMatch = text === this.pattern;
        return {
          isMatch: isMatch,
          score: isMatch ? 0 : 1,
          indices: [0, this.pattern.length - 1]
        };
      }
    }], [{
      key: "type",
      get: function get() {
        return 'exact';
      }
    }, {
      key: "multiRegex",
      get: function get() {
        return /^="(.*)"$/;
      }
    }, {
      key: "singleRegex",
      get: function get() {
        return /^=(.*)$/;
      }
    }]);

    return ExactMatch;
  }(BaseMatch);

  var InverseExactMatch = /*#__PURE__*/function (_BaseMatch) {
    _inherits(InverseExactMatch, _BaseMatch);

    var _super = _createSuper(InverseExactMatch);

    function InverseExactMatch(pattern) {
      _classCallCheck(this, InverseExactMatch);

      return _super.call(this, pattern);
    }

    _createClass(InverseExactMatch, [{
      key: "search",
      value: function search(text) {
        var index = text.indexOf(this.pattern);
        var isMatch = index === -1;
        return {
          isMatch: isMatch,
          score: isMatch ? 0 : 1,
          indices: [0, text.length - 1]
        };
      }
    }], [{
      key: "type",
      get: function get() {
        return 'inverse-exact';
      }
    }, {
      key: "multiRegex",
      get: function get() {
        return /^!"(.*)"$/;
      }
    }, {
      key: "singleRegex",
      get: function get() {
        return /^!(.*)$/;
      }
    }]);

    return InverseExactMatch;
  }(BaseMatch);

  var PrefixExactMatch = /*#__PURE__*/function (_BaseMatch) {
    _inherits(PrefixExactMatch, _BaseMatch);

    var _super = _createSuper(PrefixExactMatch);

    function PrefixExactMatch(pattern) {
      _classCallCheck(this, PrefixExactMatch);

      return _super.call(this, pattern);
    }

    _createClass(PrefixExactMatch, [{
      key: "search",
      value: function search(text) {
        var isMatch = text.startsWith(this.pattern);
        return {
          isMatch: isMatch,
          score: isMatch ? 0 : 1,
          indices: [0, this.pattern.length - 1]
        };
      }
    }], [{
      key: "type",
      get: function get() {
        return 'prefix-exact';
      }
    }, {
      key: "multiRegex",
      get: function get() {
        return /^\^"(.*)"$/;
      }
    }, {
      key: "singleRegex",
      get: function get() {
        return /^\^(.*)$/;
      }
    }]);

    return PrefixExactMatch;
  }(BaseMatch);

  var InversePrefixExactMatch = /*#__PURE__*/function (_BaseMatch) {
    _inherits(InversePrefixExactMatch, _BaseMatch);

    var _super = _createSuper(InversePrefixExactMatch);

    function InversePrefixExactMatch(pattern) {
      _classCallCheck(this, InversePrefixExactMatch);

      return _super.call(this, pattern);
    }

    _createClass(InversePrefixExactMatch, [{
      key: "search",
      value: function search(text) {
        var isMatch = !text.startsWith(this.pattern);
        return {
          isMatch: isMatch,
          score: isMatch ? 0 : 1,
          indices: [0, text.length - 1]
        };
      }
    }], [{
      key: "type",
      get: function get() {
        return 'inverse-prefix-exact';
      }
    }, {
      key: "multiRegex",
      get: function get() {
        return /^!\^"(.*)"$/;
      }
    }, {
      key: "singleRegex",
      get: function get() {
        return /^!\^(.*)$/;
      }
    }]);

    return InversePrefixExactMatch;
  }(BaseMatch);

  var SuffixExactMatch = /*#__PURE__*/function (_BaseMatch) {
    _inherits(SuffixExactMatch, _BaseMatch);

    var _super = _createSuper(SuffixExactMatch);

    function SuffixExactMatch(pattern) {
      _classCallCheck(this, SuffixExactMatch);

      return _super.call(this, pattern);
    }

    _createClass(SuffixExactMatch, [{
      key: "search",
      value: function search(text) {
        var isMatch = text.endsWith(this.pattern);
        return {
          isMatch: isMatch,
          score: isMatch ? 0 : 1,
          indices: [text.length - this.pattern.length, text.length - 1]
        };
      }
    }], [{
      key: "type",
      get: function get() {
        return 'suffix-exact';
      }
    }, {
      key: "multiRegex",
      get: function get() {
        return /^"(.*)"\$$/;
      }
    }, {
      key: "singleRegex",
      get: function get() {
        return /^(.*)\$$/;
      }
    }]);

    return SuffixExactMatch;
  }(BaseMatch);

  var InverseSuffixExactMatch = /*#__PURE__*/function (_BaseMatch) {
    _inherits(InverseSuffixExactMatch, _BaseMatch);

    var _super = _createSuper(InverseSuffixExactMatch);

    function InverseSuffixExactMatch(pattern) {
      _classCallCheck(this, InverseSuffixExactMatch);

      return _super.call(this, pattern);
    }

    _createClass(InverseSuffixExactMatch, [{
      key: "search",
      value: function search(text) {
        var isMatch = !text.endsWith(this.pattern);
        return {
          isMatch: isMatch,
          score: isMatch ? 0 : 1,
          indices: [0, text.length - 1]
        };
      }
    }], [{
      key: "type",
      get: function get() {
        return 'inverse-suffix-exact';
      }
    }, {
      key: "multiRegex",
      get: function get() {
        return /^!"(.*)"\$$/;
      }
    }, {
      key: "singleRegex",
      get: function get() {
        return /^!(.*)\$$/;
      }
    }]);

    return InverseSuffixExactMatch;
  }(BaseMatch);

  var FuzzyMatch = /*#__PURE__*/function (_BaseMatch) {
    _inherits(FuzzyMatch, _BaseMatch);

    var _super = _createSuper(FuzzyMatch);

    function FuzzyMatch(pattern) {
      var _this;

      var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
          _ref$location = _ref.location,
          location = _ref$location === void 0 ? Config.location : _ref$location,
          _ref$threshold = _ref.threshold,
          threshold = _ref$threshold === void 0 ? Config.threshold : _ref$threshold,
          _ref$distance = _ref.distance,
          distance = _ref$distance === void 0 ? Config.distance : _ref$distance,
          _ref$includeMatches = _ref.includeMatches,
          includeMatches = _ref$includeMatches === void 0 ? Config.includeMatches : _ref$includeMatches,
          _ref$findAllMatches = _ref.findAllMatches,
          findAllMatches = _ref$findAllMatches === void 0 ? Config.findAllMatches : _ref$findAllMatches,
          _ref$minMatchCharLeng = _ref.minMatchCharLength,
          minMatchCharLength = _ref$minMatchCharLeng === void 0 ? Config.minMatchCharLength : _ref$minMatchCharLeng,
          _ref$isCaseSensitive = _ref.isCaseSensitive,
          isCaseSensitive = _ref$isCaseSensitive === void 0 ? Config.isCaseSensitive : _ref$isCaseSensitive,
          _ref$ignoreLocation = _ref.ignoreLocation,
          ignoreLocation = _ref$ignoreLocation === void 0 ? Config.ignoreLocation : _ref$ignoreLocation;

      _classCallCheck(this, FuzzyMatch);

      _this = _super.call(this, pattern);
      _this._bitapSearch = new BitapSearch(pattern, {
        location: location,
        threshold: threshold,
        distance: distance,
        includeMatches: includeMatches,
        findAllMatches: findAllMatches,
        minMatchCharLength: minMatchCharLength,
        isCaseSensitive: isCaseSensitive,
        ignoreLocation: ignoreLocation
      });
      return _this;
    }

    _createClass(FuzzyMatch, [{
      key: "search",
      value: function search(text) {
        return this._bitapSearch.searchIn(text);
      }
    }], [{
      key: "type",
      get: function get() {
        return 'fuzzy';
      }
    }, {
      key: "multiRegex",
      get: function get() {
        return /^"(.*)"$/;
      }
    }, {
      key: "singleRegex",
      get: function get() {
        return /^(.*)$/;
      }
    }]);

    return FuzzyMatch;
  }(BaseMatch);

  var IncludeMatch = /*#__PURE__*/function (_BaseMatch) {
    _inherits(IncludeMatch, _BaseMatch);

    var _super = _createSuper(IncludeMatch);

    function IncludeMatch(pattern) {
      _classCallCheck(this, IncludeMatch);

      return _super.call(this, pattern);
    }

    _createClass(IncludeMatch, [{
      key: "search",
      value: function search(text) {
        var location = 0;
        var index;
        var indices = [];
        var patternLen = this.pattern.length; // Get all exact matches

        while ((index = text.indexOf(this.pattern, location)) > -1) {
          location = index + patternLen;
          indices.push([index, location - 1]);
        }

        var isMatch = !!indices.length;
        return {
          isMatch: isMatch,
          score: isMatch ? 0 : 1,
          indices: indices
        };
      }
    }], [{
      key: "type",
      get: function get() {
        return 'include';
      }
    }, {
      key: "multiRegex",
      get: function get() {
        return /^'"(.*)"$/;
      }
    }, {
      key: "singleRegex",
      get: function get() {
        return /^'(.*)$/;
      }
    }]);

    return IncludeMatch;
  }(BaseMatch);

  var searchers = [ExactMatch, IncludeMatch, PrefixExactMatch, InversePrefixExactMatch, InverseSuffixExactMatch, SuffixExactMatch, InverseExactMatch, FuzzyMatch];
  var searchersLen = searchers.length; // Regex to split by spaces, but keep anything in quotes together

  var SPACE_RE = / +(?=(?:[^\"]*\"[^\"]*\")*[^\"]*$)/;
  var OR_TOKEN = '|'; // Return a 2D array representation of the query, for simpler parsing.
  // Example:
  // "^core go$ | rb$ | py$ xy$" => [["^core", "go$"], ["rb$"], ["py$", "xy$"]]

  function parseQuery(pattern) {
    var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
    return pattern.split(OR_TOKEN).map(function (item) {
      var query = item.trim().split(SPACE_RE).filter(function (item) {
        return item && !!item.trim();
      });
      var results = [];

      for (var i = 0, len = query.length; i < len; i += 1) {
        var queryItem = query[i]; // 1. Handle multiple query match (i.e, once that are quoted, like `"hello world"`)

        var found = false;
        var idx = -1;

        while (!found && ++idx < searchersLen) {
          var searcher = searchers[idx];
          var token = searcher.isMultiMatch(queryItem);

          if (token) {
            results.push(new searcher(token, options));
            found = true;
          }
        }

        if (found) {
          continue;
        } // 2. Handle single query matches (i.e, once that are *not* quoted)


        idx = -1;

        while (++idx < searchersLen) {
          var _searcher = searchers[idx];

          var _token = _searcher.isSingleMatch(queryItem);

          if (_token) {
            results.push(new _searcher(_token, options));
            break;
          }
        }
      }

      return results;
    });
  }

  // to a singl match

  var MultiMatchSet = new Set([FuzzyMatch.type, IncludeMatch.type]);
  /**
   * Command-like searching
   * ======================
   *
   * Given multiple search terms delimited by spaces.e.g. `^jscript .python$ ruby !java`,
   * search in a given text.
   *
   * Search syntax:
   *
   * | Token       | Match type                 | Description                            |
   * | ----------- | -------------------------- | -------------------------------------- |
   * | `jscript`   | fuzzy-match                | Items that fuzzy match `jscript`       |
   * | `=scheme`   | exact-match                | Items that are `scheme`                |
   * | `'python`   | include-match              | Items that include `python`            |
   * | `!ruby`     | inverse-exact-match        | Items that do not include `ruby`       |
   * | `^java`     | prefix-exact-match         | Items that start with `java`           |
   * | `!^earlang` | inverse-prefix-exact-match | Items that do not start with `earlang` |
   * | `.js$`      | suffix-exact-match         | Items that end with `.js`              |
   * | `!.go$`     | inverse-suffix-exact-match | Items that do not end with `.go`       |
   *
   * A single pipe character acts as an OR operator. For example, the following
   * query matches entries that start with `core` and end with either`go`, `rb`,
   * or`py`.
   *
   * ```
   * ^core go$ | rb$ | py$
   * ```
   */

  var ExtendedSearch = /*#__PURE__*/function () {
    function ExtendedSearch(pattern) {
      var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
          _ref$isCaseSensitive = _ref.isCaseSensitive,
          isCaseSensitive = _ref$isCaseSensitive === void 0 ? Config.isCaseSensitive : _ref$isCaseSensitive,
          _ref$includeMatches = _ref.includeMatches,
          includeMatches = _ref$includeMatches === void 0 ? Config.includeMatches : _ref$includeMatches,
          _ref$minMatchCharLeng = _ref.minMatchCharLength,
          minMatchCharLength = _ref$minMatchCharLeng === void 0 ? Config.minMatchCharLength : _ref$minMatchCharLeng,
          _ref$ignoreLocation = _ref.ignoreLocation,
          ignoreLocation = _ref$ignoreLocation === void 0 ? Config.ignoreLocation : _ref$ignoreLocation,
          _ref$findAllMatches = _ref.findAllMatches,
          findAllMatches = _ref$findAllMatches === void 0 ? Config.findAllMatches : _ref$findAllMatches,
          _ref$location = _ref.location,
          location = _ref$location === void 0 ? Config.location : _ref$location,
          _ref$threshold = _ref.threshold,
          threshold = _ref$threshold === void 0 ? Config.threshold : _ref$threshold,
          _ref$distance = _ref.distance,
          distance = _ref$distance === void 0 ? Config.distance : _ref$distance;

      _classCallCheck(this, ExtendedSearch);

      this.query = null;
      this.options = {
        isCaseSensitive: isCaseSensitive,
        includeMatches: includeMatches,
        minMatchCharLength: minMatchCharLength,
        findAllMatches: findAllMatches,
        ignoreLocation: ignoreLocation,
        location: location,
        threshold: threshold,
        distance: distance
      };
      this.pattern = isCaseSensitive ? pattern : pattern.toLowerCase();
      this.query = parseQuery(this.pattern, this.options);
    }

    _createClass(ExtendedSearch, [{
      key: "searchIn",
      value: function searchIn(text) {
        var query = this.query;

        if (!query) {
          return {
            isMatch: false,
            score: 1
          };
        }

        var _this$options = this.options,
            includeMatches = _this$options.includeMatches,
            isCaseSensitive = _this$options.isCaseSensitive;
        text = isCaseSensitive ? text : text.toLowerCase();
        var numMatches = 0;
        var allIndices = [];
        var totalScore = 0; // ORs

        for (var i = 0, qLen = query.length; i < qLen; i += 1) {
          var searchers = query[i]; // Reset indices

          allIndices.length = 0;
          numMatches = 0; // ANDs

          for (var j = 0, pLen = searchers.length; j < pLen; j += 1) {
            var searcher = searchers[j];

            var _searcher$search = searcher.search(text),
                isMatch = _searcher$search.isMatch,
                indices = _searcher$search.indices,
                score = _searcher$search.score;

            if (isMatch) {
              numMatches += 1;
              totalScore += score;

              if (includeMatches) {
                var type = searcher.constructor.type;

                if (MultiMatchSet.has(type)) {
                  allIndices = [].concat(_toConsumableArray(allIndices), _toConsumableArray(indices));
                } else {
                  allIndices.push(indices);
                }
              }
            } else {
              totalScore = 0;
              numMatches = 0;
              allIndices.length = 0;
              break;
            }
          } // OR condition, so if TRUE, return


          if (numMatches) {
            var result = {
              isMatch: true,
              score: totalScore / numMatches
            };

            if (includeMatches) {
              result.indices = allIndices;
            }

            return result;
          }
        } // Nothing was matched


        return {
          isMatch: false,
          score: 1
        };
      }
    }], [{
      key: "condition",
      value: function condition(_, options) {
        return options.useExtendedSearch;
      }
    }]);

    return ExtendedSearch;
  }();

  var registeredSearchers = [];
  function register() {
    registeredSearchers.push.apply(registeredSearchers, arguments);
  }
  function createSearcher(pattern, options) {
    for (var i = 0, len = registeredSearchers.length; i < len; i += 1) {
      var searcherClass = registeredSearchers[i];

      if (searcherClass.condition(pattern, options)) {
        return new searcherClass(pattern, options);
      }
    }

    return new BitapSearch(pattern, options);
  }

  var LogicalOperator = {
    AND: '$and',
    OR: '$or'
  };
  var KeyType = {
    PATH: '$path',
    PATTERN: '$val'
  };

  var isExpression = function isExpression(query) {
    return !!(query[LogicalOperator.AND] || query[LogicalOperator.OR]);
  };

  var isPath = function isPath(query) {
    return !!query[KeyType.PATH];
  };

  var isLeaf = function isLeaf(query) {
    return !isArray(query) && isObject(query) && !isExpression(query);
  };

  var convertToExplicit = function convertToExplicit(query) {
    return _defineProperty({}, LogicalOperator.AND, Object.keys(query).map(function (key) {
      return _defineProperty({}, key, query[key]);
    }));
  }; // When `auto` is `true`, the parse function will infer and initialize and add
  // the appropriate `Searcher` instance


  function parse(query, options) {
    var _ref3 = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
        _ref3$auto = _ref3.auto,
        auto = _ref3$auto === void 0 ? true : _ref3$auto;

    var next = function next(query) {
      var keys = Object.keys(query);
      var isQueryPath = isPath(query);

      if (!isQueryPath && keys.length > 1 && !isExpression(query)) {
        return next(convertToExplicit(query));
      }

      if (isLeaf(query)) {
        var key = isQueryPath ? query[KeyType.PATH] : keys[0];
        var pattern = isQueryPath ? query[KeyType.PATTERN] : query[key];

        if (!isString(pattern)) {
          throw new Error(LOGICAL_SEARCH_INVALID_QUERY_FOR_KEY(key));
        }

        var obj = {
          keyId: createKeyId(key),
          pattern: pattern
        };

        if (auto) {
          obj.searcher = createSearcher(pattern, options);
        }

        return obj;
      }

      var node = {
        children: [],
        operator: keys[0]
      };
      keys.forEach(function (key) {
        var value = query[key];

        if (isArray(value)) {
          value.forEach(function (item) {
            node.children.push(next(item));
          });
        }
      });
      return node;
    };

    if (!isExpression(query)) {
      query = convertToExplicit(query);
    }

    return next(query);
  }

  function computeScore(results, _ref) {
    var _ref$ignoreFieldNorm = _ref.ignoreFieldNorm,
        ignoreFieldNorm = _ref$ignoreFieldNorm === void 0 ? Config.ignoreFieldNorm : _ref$ignoreFieldNorm;
    results.forEach(function (result) {
      var totalScore = 1;
      result.matches.forEach(function (_ref2) {
        var key = _ref2.key,
            norm = _ref2.norm,
            score = _ref2.score;
        var weight = key ? key.weight : null;
        totalScore *= Math.pow(score === 0 && weight ? Number.EPSILON : score, (weight || 1) * (ignoreFieldNorm ? 1 : norm));
      });
      result.score = totalScore;
    });
  }

  function transformMatches(result, data) {
    var matches = result.matches;
    data.matches = [];

    if (!isDefined(matches)) {
      return;
    }

    matches.forEach(function (match) {
      if (!isDefined(match.indices) || !match.indices.length) {
        return;
      }

      var indices = match.indices,
          value = match.value;
      var obj = {
        indices: indices,
        value: value
      };

      if (match.key) {
        obj.key = match.key.src;
      }

      if (match.idx > -1) {
        obj.refIndex = match.idx;
      }

      data.matches.push(obj);
    });
  }

  function transformScore(result, data) {
    data.score = result.score;
  }

  function format(results, docs) {
    var _ref = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : {},
        _ref$includeMatches = _ref.includeMatches,
        includeMatches = _ref$includeMatches === void 0 ? Config.includeMatches : _ref$includeMatches,
        _ref$includeScore = _ref.includeScore,
        includeScore = _ref$includeScore === void 0 ? Config.includeScore : _ref$includeScore;

    var transformers = [];
    if (includeMatches) transformers.push(transformMatches);
    if (includeScore) transformers.push(transformScore);
    return results.map(function (result) {
      var idx = result.idx;
      var data = {
        item: docs[idx],
        refIndex: idx
      };

      if (transformers.length) {
        transformers.forEach(function (transformer) {
          transformer(result, data);
        });
      }

      return data;
    });
  }

  var Fuse$1 = /*#__PURE__*/function () {
    function Fuse(docs) {
      var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
      var index = arguments.length > 2 ? arguments[2] : undefined;

      _classCallCheck(this, Fuse);

      this.options = _objectSpread2(_objectSpread2({}, Config), options);

      if (this.options.useExtendedSearch && !true) {
        throw new Error(EXTENDED_SEARCH_UNAVAILABLE);
      }

      this._keyStore = new KeyStore(this.options.keys);
      this.setCollection(docs, index);
    }

    _createClass(Fuse, [{
      key: "setCollection",
      value: function setCollection(docs, index) {
        this._docs = docs;

        if (index && !(index instanceof FuseIndex)) {
          throw new Error(INCORRECT_INDEX_TYPE);
        }

        this._myIndex = index || createIndex(this.options.keys, this._docs, {
          getFn: this.options.getFn,
          fieldNormWeight: this.options.fieldNormWeight
        });
      }
    }, {
      key: "add",
      value: function add(doc) {
        if (!isDefined(doc)) {
          return;
        }

        this._docs.push(doc);

        this._myIndex.add(doc);
      }
    }, {
      key: "remove",
      value: function remove() {
        var predicate = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : function
          /* doc, idx */
        () {
          return false;
        };
        var results = [];

        for (var i = 0, len = this._docs.length; i < len; i += 1) {
          var doc = this._docs[i];

          if (predicate(doc, i)) {
            this.removeAt(i);
            i -= 1;
            len -= 1;
            results.push(doc);
          }
        }

        return results;
      }
    }, {
      key: "removeAt",
      value: function removeAt(idx) {
        this._docs.splice(idx, 1);

        this._myIndex.removeAt(idx);
      }
    }, {
      key: "getIndex",
      value: function getIndex() {
        return this._myIndex;
      }
    }, {
      key: "search",
      value: function search(query) {
        var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
            _ref$limit = _ref.limit,
            limit = _ref$limit === void 0 ? -1 : _ref$limit;

        var _this$options = this.options,
            includeMatches = _this$options.includeMatches,
            includeScore = _this$options.includeScore,
            shouldSort = _this$options.shouldSort,
            sortFn = _this$options.sortFn,
            ignoreFieldNorm = _this$options.ignoreFieldNorm;
        var results = isString(query) ? isString(this._docs[0]) ? this._searchStringList(query) : this._searchObjectList(query) : this._searchLogical(query);
        computeScore(results, {
          ignoreFieldNorm: ignoreFieldNorm
        });

        if (shouldSort) {
          results.sort(sortFn);
        }

        if (isNumber(limit) && limit > -1) {
          results = results.slice(0, limit);
        }

        return format(results, this._docs, {
          includeMatches: includeMatches,
          includeScore: includeScore
        });
      }
    }, {
      key: "_searchStringList",
      value: function _searchStringList(query) {
        var searcher = createSearcher(query, this.options);
        var records = this._myIndex.records;
        var results = []; // Iterate over every string in the index

        records.forEach(function (_ref2) {
          var text = _ref2.v,
              idx = _ref2.i,
              norm = _ref2.n;

          if (!isDefined(text)) {
            return;
          }

          var _searcher$searchIn = searcher.searchIn(text),
              isMatch = _searcher$searchIn.isMatch,
              score = _searcher$searchIn.score,
              indices = _searcher$searchIn.indices;

          if (isMatch) {
            results.push({
              item: text,
              idx: idx,
              matches: [{
                score: score,
                value: text,
                norm: norm,
                indices: indices
              }]
            });
          }
        });
        return results;
      }
    }, {
      key: "_searchLogical",
      value: function _searchLogical(query) {
        var _this = this;

        var expression = parse(query, this.options);

        var evaluate = function evaluate(node, item, idx) {
          if (!node.children) {
            var keyId = node.keyId,
                searcher = node.searcher;

            var matches = _this._findMatches({
              key: _this._keyStore.get(keyId),
              value: _this._myIndex.getValueForItemAtKeyId(item, keyId),
              searcher: searcher
            });

            if (matches && matches.length) {
              return [{
                idx: idx,
                item: item,
                matches: matches
              }];
            }

            return [];
          }

          var res = [];

          for (var i = 0, len = node.children.length; i < len; i += 1) {
            var child = node.children[i];
            var result = evaluate(child, item, idx);

            if (result.length) {
              res.push.apply(res, _toConsumableArray(result));
            } else if (node.operator === LogicalOperator.AND) {
              return [];
            }
          }

          return res;
        };

        var records = this._myIndex.records;
        var resultMap = {};
        var results = [];
        records.forEach(function (_ref3) {
          var item = _ref3.$,
              idx = _ref3.i;

          if (isDefined(item)) {
            var expResults = evaluate(expression, item, idx);

            if (expResults.length) {
              // Dedupe when adding
              if (!resultMap[idx]) {
                resultMap[idx] = {
                  idx: idx,
                  item: item,
                  matches: []
                };
                results.push(resultMap[idx]);
              }

              expResults.forEach(function (_ref4) {
                var _resultMap$idx$matche;

                var matches = _ref4.matches;

                (_resultMap$idx$matche = resultMap[idx].matches).push.apply(_resultMap$idx$matche, _toConsumableArray(matches));
              });
            }
          }
        });
        return results;
      }
    }, {
      key: "_searchObjectList",
      value: function _searchObjectList(query) {
        var _this2 = this;

        var searcher = createSearcher(query, this.options);
        var _this$_myIndex = this._myIndex,
            keys = _this$_myIndex.keys,
            records = _this$_myIndex.records;
        var results = []; // List is Array<Object>

        records.forEach(function (_ref5) {
          var item = _ref5.$,
              idx = _ref5.i;

          if (!isDefined(item)) {
            return;
          }

          var matches = []; // Iterate over every key (i.e, path), and fetch the value at that key

          keys.forEach(function (key, keyIndex) {
            matches.push.apply(matches, _toConsumableArray(_this2._findMatches({
              key: key,
              value: item[keyIndex],
              searcher: searcher
            })));
          });

          if (matches.length) {
            results.push({
              idx: idx,
              item: item,
              matches: matches
            });
          }
        });
        return results;
      }
    }, {
      key: "_findMatches",
      value: function _findMatches(_ref6) {
        var key = _ref6.key,
            value = _ref6.value,
            searcher = _ref6.searcher;

        if (!isDefined(value)) {
          return [];
        }

        var matches = [];

        if (isArray(value)) {
          value.forEach(function (_ref7) {
            var text = _ref7.v,
                idx = _ref7.i,
                norm = _ref7.n;

            if (!isDefined(text)) {
              return;
            }

            var _searcher$searchIn2 = searcher.searchIn(text),
                isMatch = _searcher$searchIn2.isMatch,
                score = _searcher$searchIn2.score,
                indices = _searcher$searchIn2.indices;

            if (isMatch) {
              matches.push({
                score: score,
                key: key,
                value: text,
                idx: idx,
                norm: norm,
                indices: indices
              });
            }
          });
        } else {
          var text = value.v,
              norm = value.n;

          var _searcher$searchIn3 = searcher.searchIn(text),
              isMatch = _searcher$searchIn3.isMatch,
              score = _searcher$searchIn3.score,
              indices = _searcher$searchIn3.indices;

          if (isMatch) {
            matches.push({
              score: score,
              key: key,
              value: text,
              norm: norm,
              indices: indices
            });
          }
        }

        return matches;
      }
    }]);

    return Fuse;
  }();

  Fuse$1.version = '6.6.2';
  Fuse$1.createIndex = createIndex;
  Fuse$1.parseIndex = parseIndex;
  Fuse$1.config = Config;

  {
    Fuse$1.parseQuery = parse;
  }

  {
    register(ExtendedSearch);
  }

  var Fuse = Fuse$1;

  return Fuse;

}));

/**
 * 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(mcService.storage.get(name + uin));
                        } else {
                            return mcService.storage.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(mcService.storage.remove(name + uin));
                        } else {
                            return mcService.storage.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(mcService.storage.put(name + uin, data));
                        } else {
                            return mcService.storage.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(mcService.storage.put(ID, JSON.stringify(globalOptions)));
                } else {
                    return mcService.storage.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 = mcService.storage.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));
                }
            });
        }
    }

    function put(name, value) {

    };

    $rootScope.Storage = {
        put: put,
        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(mcService.storage.get(name + uin));
                        } else {
                            return mcService.storage.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(mcService.storage.remove(name + uin));
                        } else {
                            return mcService.storage.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(mcService.storage.put(name + uin, data));
                        } else {
                            return mcService.storage.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);


;(function (root, factory) {
	if (typeof exports === "object") {
		// CommonJS
		module.exports = exports = factory();
	}
	else if (typeof define === "function" && define.amd) {
		// AMD
		define([], factory);
	}
	else {
		// Global (browser)
		root.CryptoJS = factory();
	}
}(this, function () {

	/*globals window, global, require*/

	/**
	 * CryptoJS core components.
	 */
	var CryptoJS = CryptoJS || (function (Math, undefined) {

	    var crypto;

	    // Native crypto from window (Browser)
	    if (typeof window !== 'undefined' && window.crypto) {
	        crypto = window.crypto;
	    }

	    // Native crypto in web worker (Browser)
	    if (typeof self !== 'undefined' && self.crypto) {
	        crypto = self.crypto;
	    }

	    // Native crypto from worker
	    if (typeof globalThis !== 'undefined' && globalThis.crypto) {
	        crypto = globalThis.crypto;
	    }

	    // Native (experimental IE 11) crypto from window (Browser)
	    if (!crypto && typeof window !== 'undefined' && window.msCrypto) {
	        crypto = window.msCrypto;
	    }

	    // Native crypto from global (NodeJS)
	    if (!crypto && typeof global !== 'undefined' && global.crypto) {
	        crypto = global.crypto;
	    }

	    // Native crypto import via require (NodeJS)
	    if (!crypto && typeof require === 'function') {
	        try {
	            crypto = require('crypto');
	        } catch (err) {}
	    }

	    /*
	     * Cryptographically secure pseudorandom number generator
	     *
	     * As Math.random() is cryptographically not safe to use
	     */
	    var cryptoSecureRandomInt = function () {
	        if (crypto) {
	            // Use getRandomValues method (Browser)
	            if (typeof crypto.getRandomValues === 'function') {
	                try {
	                    return crypto.getRandomValues(new Uint32Array(1))[0];
	                } catch (err) {}
	            }

	            // Use randomBytes method (NodeJS)
	            if (typeof crypto.randomBytes === 'function') {
	                try {
	                    return crypto.randomBytes(4).readInt32LE();
	                } catch (err) {}
	            }
	        }

	        throw new Error('Native crypto module could not be used to get secure random number.');
	    };

	    /*
	     * Local polyfill of Object.create

	     */
	    var create = Object.create || (function () {
	        function F() {}

	        return function (obj) {
	            var subtype;

	            F.prototype = obj;

	            subtype = new F();

	            F.prototype = null;

	            return subtype;
	        };
	    }());

	    /**
	     * CryptoJS namespace.
	     */
	    var C = {};

	    /**
	     * Library namespace.
	     */
	    var C_lib = C.lib = {};

	    /**
	     * Base object for prototypal inheritance.
	     */
	    var Base = C_lib.Base = (function () {


	        return {
	            /**
	             * Creates a new object that inherits from this object.
	             *
	             * @param {Object} overrides Properties to copy into the new object.
	             *
	             * @return {Object} The new object.
	             *
	             * @static
	             *
	             * @example
	             *
	             *     var MyType = CryptoJS.lib.Base.extend({
	             *         field: 'value',
	             *
	             *         method: function () {
	             *         }
	             *     });
	             */
	            extend: function (overrides) {
	                // Spawn
	                var subtype = create(this);

	                // Augment
	                if (overrides) {
	                    subtype.mixIn(overrides);
	                }

	                // Create default initializer
	                if (!subtype.hasOwnProperty('init') || this.init === subtype.init) {
	                    subtype.init = function () {
	                        subtype.$super.init.apply(this, arguments);
	                    };
	                }

	                // Initializer's prototype is the subtype object
	                subtype.init.prototype = subtype;

	                // Reference supertype
	                subtype.$super = this;

	                return subtype;
	            },

	            /**
	             * Extends this object and runs the init method.
	             * Arguments to create() will be passed to init().
	             *
	             * @return {Object} The new object.
	             *
	             * @static
	             *
	             * @example
	             *
	             *     var instance = MyType.create();
	             */
	            create: function () {
	                var instance = this.extend();
	                instance.init.apply(instance, arguments);

	                return instance;
	            },

	            /**
	             * Initializes a newly created object.
	             * Override this method to add some logic when your objects are created.
	             *
	             * @example
	             *
	             *     var MyType = CryptoJS.lib.Base.extend({
	             *         init: function () {
	             *             // ...
	             *         }
	             *     });
	             */
	            init: function () {
	            },

	            /**
	             * Copies properties into this object.
	             *
	             * @param {Object} properties The properties to mix in.
	             *
	             * @example
	             *
	             *     MyType.mixIn({
	             *         field: 'value'
	             *     });
	             */
	            mixIn: function (properties) {
	                for (var propertyName in properties) {
	                    if (properties.hasOwnProperty(propertyName)) {
	                        this[propertyName] = properties[propertyName];
	                    }
	                }

	                // IE won't copy toString using the loop above
	                if (properties.hasOwnProperty('toString')) {
	                    this.toString = properties.toString;
	                }
	            },

	            /**
	             * Creates a copy of this object.
	             *
	             * @return {Object} The clone.
	             *
	             * @example
	             *
	             *     var clone = instance.clone();
	             */
	            clone: function () {
	                return this.init.prototype.extend(this);
	            }
	        };
	    }());

	    /**
	     * An array of 32-bit words.
	     *
	     * @property {Array} words The array of 32-bit words.
	     * @property {number} sigBytes The number of significant bytes in this word array.
	     */
	    var WordArray = C_lib.WordArray = Base.extend({
	        /**
	         * Initializes a newly created word array.
	         *
	         * @param {Array} words (Optional) An array of 32-bit words.
	         * @param {number} sigBytes (Optional) The number of significant bytes in the words.
	         *
	         * @example
	         *
	         *     var wordArray = CryptoJS.lib.WordArray.create();
	         *     var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607]);
	         *     var wordArray = CryptoJS.lib.WordArray.create([0x00010203, 0x04050607], 6);
	         */
	        init: function (words, sigBytes) {
	            words = this.words = words || [];

	            if (sigBytes != undefined) {
	                this.sigBytes = sigBytes;
	            } else {
	                this.sigBytes = words.length * 4;
	            }
	        },

	        /**
	         * Converts this word array to a string.
	         *
	         * @param {Encoder} encoder (Optional) The encoding strategy to use. Default: CryptoJS.enc.Hex
	         *
	         * @return {string} The stringified word array.
	         *
	         * @example
	         *
	         *     var string = wordArray + '';
	         *     var string = wordArray.toString();
	         *     var string = wordArray.toString(CryptoJS.enc.Utf8);
	         */
	        toString: function (encoder) {
	            return (encoder || Hex).stringify(this);
	        },

	        /**
	         * Concatenates a word array to this word array.
	         *
	         * @param {WordArray} wordArray The word array to append.
	         *
	         * @return {WordArray} This word array.
	         *
	         * @example
	         *
	         *     wordArray1.concat(wordArray2);
	         */
	        concat: function (wordArray) {
	            // Shortcuts
	            var thisWords = this.words;
	            var thatWords = wordArray.words;
	            var thisSigBytes = this.sigBytes;
	            var thatSigBytes = wordArray.sigBytes;

	            // Clamp excess bits
	            this.clamp();

	            // Concat
	            if (thisSigBytes % 4) {
	                // Copy one byte at a time
	                for (var i = 0; i < thatSigBytes; i++) {
	                    var thatByte = (thatWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
	                    thisWords[(thisSigBytes + i) >>> 2] |= thatByte << (24 - ((thisSigBytes + i) % 4) * 8);
	                }
	            } else {
	                // Copy one word at a time
	                for (var j = 0; j < thatSigBytes; j += 4) {
	                    thisWords[(thisSigBytes + j) >>> 2] = thatWords[j >>> 2];
	                }
	            }
	            this.sigBytes += thatSigBytes;

	            // Chainable
	            return this;
	        },

	        /**
	         * Removes insignificant bits.
	         *
	         * @example
	         *
	         *     wordArray.clamp();
	         */
	        clamp: function () {
	            // Shortcuts
	            var words = this.words;
	            var sigBytes = this.sigBytes;

	            // Clamp
	            words[sigBytes >>> 2] &= 0xffffffff << (32 - (sigBytes % 4) * 8);
	            words.length = Math.ceil(sigBytes / 4);
	        },

	        /**
	         * Creates a copy of this word array.
	         *
	         * @return {WordArray} The clone.
	         *
	         * @example
	         *
	         *     var clone = wordArray.clone();
	         */
	        clone: function () {
	            var clone = Base.clone.call(this);
	            clone.words = this.words.slice(0);

	            return clone;
	        },

	        /**
	         * Creates a word array filled with random bytes.
	         *
	         * @param {number} nBytes The number of random bytes to generate.
	         *
	         * @return {WordArray} The random word array.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var wordArray = CryptoJS.lib.WordArray.random(16);
	         */
	        random: function (nBytes) {
	            var words = [];

	            for (var i = 0; i < nBytes; i += 4) {
	                words.push(cryptoSecureRandomInt());
	            }

	            return new WordArray.init(words, nBytes);
	        }
	    });

	    /**
	     * Encoder namespace.
	     */
	    var C_enc = C.enc = {};

	    /**
	     * Hex encoding strategy.
	     */
	    var Hex = C_enc.Hex = {
	        /**
	         * Converts a word array to a hex string.
	         *
	         * @param {WordArray} wordArray The word array.
	         *
	         * @return {string} The hex string.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var hexString = CryptoJS.enc.Hex.stringify(wordArray);
	         */
	        stringify: function (wordArray) {
	            // Shortcuts
	            var words = wordArray.words;
	            var sigBytes = wordArray.sigBytes;

	            // Convert
	            var hexChars = [];
	            for (var i = 0; i < sigBytes; i++) {
	                var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
	                hexChars.push((bite >>> 4).toString(16));
	                hexChars.push((bite & 0x0f).toString(16));
	            }

	            return hexChars.join('');
	        },

	        /**
	         * Converts a hex string to a word array.
	         *
	         * @param {string} hexStr The hex string.
	         *
	         * @return {WordArray} The word array.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var wordArray = CryptoJS.enc.Hex.parse(hexString);
	         */
	        parse: function (hexStr) {
	            // Shortcut
	            var hexStrLength = hexStr.length;

	            // Convert
	            var words = [];
	            for (var i = 0; i < hexStrLength; i += 2) {
	                words[i >>> 3] |= parseInt(hexStr.substr(i, 2), 16) << (24 - (i % 8) * 4);
	            }

	            return new WordArray.init(words, hexStrLength / 2);
	        }
	    };

	    /**
	     * Latin1 encoding strategy.
	     */
	    var Latin1 = C_enc.Latin1 = {
	        /**
	         * Converts a word array to a Latin1 string.
	         *
	         * @param {WordArray} wordArray The word array.
	         *
	         * @return {string} The Latin1 string.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var latin1String = CryptoJS.enc.Latin1.stringify(wordArray);
	         */
	        stringify: function (wordArray) {
	            // Shortcuts
	            var words = wordArray.words;
	            var sigBytes = wordArray.sigBytes;

	            // Convert
	            var latin1Chars = [];
	            for (var i = 0; i < sigBytes; i++) {
	                var bite = (words[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff;
	                latin1Chars.push(String.fromCharCode(bite));
	            }

	            return latin1Chars.join('');
	        },

	        /**
	         * Converts a Latin1 string to a word array.
	         *
	         * @param {string} latin1Str The Latin1 string.
	         *
	         * @return {WordArray} The word array.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var wordArray = CryptoJS.enc.Latin1.parse(latin1String);
	         */
	        parse: function (latin1Str) {
	            // Shortcut
	            var latin1StrLength = latin1Str.length;

	            // Convert
	            var words = [];
	            for (var i = 0; i < latin1StrLength; i++) {
	                words[i >>> 2] |= (latin1Str.charCodeAt(i) & 0xff) << (24 - (i % 4) * 8);
	            }

	            return new WordArray.init(words, latin1StrLength);
	        }
	    };

	    /**
	     * UTF-8 encoding strategy.
	     */
	    var Utf8 = C_enc.Utf8 = {
	        /**
	         * Converts a word array to a UTF-8 string.
	         *
	         * @param {WordArray} wordArray The word array.
	         *
	         * @return {string} The UTF-8 string.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var utf8String = CryptoJS.enc.Utf8.stringify(wordArray);
	         */
	        stringify: function (wordArray) {
	            try {
	                return decodeURIComponent(escape(Latin1.stringify(wordArray)));
	            } catch (e) {
	                throw new Error('Malformed UTF-8 data');
	            }
	        },

	        /**
	         * Converts a UTF-8 string to a word array.
	         *
	         * @param {string} utf8Str The UTF-8 string.
	         *
	         * @return {WordArray} The word array.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var wordArray = CryptoJS.enc.Utf8.parse(utf8String);
	         */
	        parse: function (utf8Str) {
	            return Latin1.parse(unescape(encodeURIComponent(utf8Str)));
	        }
	    };

	    /**
	     * Abstract buffered block algorithm template.
	     *
	     * The property blockSize must be implemented in a concrete subtype.
	     *
	     * @property {number} _minBufferSize The number of blocks that should be kept unprocessed in the buffer. Default: 0
	     */
	    var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm = Base.extend({
	        /**
	         * Resets this block algorithm's data buffer to its initial state.
	         *
	         * @example
	         *
	         *     bufferedBlockAlgorithm.reset();
	         */
	        reset: function () {
	            // Initial values
	            this._data = new WordArray.init();
	            this._nDataBytes = 0;
	        },

	        /**
	         * Adds new data to this block algorithm's buffer.
	         *
	         * @param {WordArray|string} data The data to append. Strings are converted to a WordArray using UTF-8.
	         *
	         * @example
	         *
	         *     bufferedBlockAlgorithm._append('data');
	         *     bufferedBlockAlgorithm._append(wordArray);
	         */
	        _append: function (data) {
	            // Convert string to WordArray, else assume WordArray already
	            if (typeof data == 'string') {
	                data = Utf8.parse(data);
	            }

	            // Append
	            this._data.concat(data);
	            this._nDataBytes += data.sigBytes;
	        },

	        /**
	         * Processes available data blocks.
	         *
	         * This method invokes _doProcessBlock(offset), which must be implemented by a concrete subtype.
	         *
	         * @param {boolean} doFlush Whether all blocks and partial blocks should be processed.
	         *
	         * @return {WordArray} The processed data.
	         *
	         * @example
	         *
	         *     var processedData = bufferedBlockAlgorithm._process();
	         *     var processedData = bufferedBlockAlgorithm._process(!!'flush');
	         */
	        _process: function (doFlush) {
	            var processedWords;

	            // Shortcuts
	            var data = this._data;
	            var dataWords = data.words;
	            var dataSigBytes = data.sigBytes;
	            var blockSize = this.blockSize;
	            var blockSizeBytes = blockSize * 4;

	            // Count blocks ready
	            var nBlocksReady = dataSigBytes / blockSizeBytes;
	            if (doFlush) {
	                // Round up to include partial blocks
	                nBlocksReady = Math.ceil(nBlocksReady);
	            } else {
	                // Round down to include only full blocks,
	                // less the number of blocks that must remain in the buffer
	                nBlocksReady = Math.max((nBlocksReady | 0) - this._minBufferSize, 0);
	            }

	            // Count words ready
	            var nWordsReady = nBlocksReady * blockSize;

	            // Count bytes ready
	            var nBytesReady = Math.min(nWordsReady * 4, dataSigBytes);

	            // Process blocks
	            if (nWordsReady) {
	                for (var offset = 0; offset < nWordsReady; offset += blockSize) {
	                    // Perform concrete-algorithm logic
	                    this._doProcessBlock(dataWords, offset);
	                }

	                // Remove processed words
	                processedWords = dataWords.splice(0, nWordsReady);
	                data.sigBytes -= nBytesReady;
	            }

	            // Return processed words
	            return new WordArray.init(processedWords, nBytesReady);
	        },

	        /**
	         * Creates a copy of this object.
	         *
	         * @return {Object} The clone.
	         *
	         * @example
	         *
	         *     var clone = bufferedBlockAlgorithm.clone();
	         */
	        clone: function () {
	            var clone = Base.clone.call(this);
	            clone._data = this._data.clone();

	            return clone;
	        },

	        _minBufferSize: 0
	    });

	    /**
	     * Abstract hasher template.
	     *
	     * @property {number} blockSize The number of 32-bit words this hasher operates on. Default: 16 (512 bits)
	     */
	    var Hasher = C_lib.Hasher = BufferedBlockAlgorithm.extend({
	        /**
	         * Configuration options.
	         */
	        cfg: Base.extend(),

	        /**
	         * Initializes a newly created hasher.
	         *
	         * @param {Object} cfg (Optional) The configuration options to use for this hash computation.
	         *
	         * @example
	         *
	         *     var hasher = CryptoJS.algo.SHA256.create();
	         */
	        init: function (cfg) {
	            // Apply config defaults
	            this.cfg = this.cfg.extend(cfg);

	            // Set initial values
	            this.reset();
	        },

	        /**
	         * Resets this hasher to its initial state.
	         *
	         * @example
	         *
	         *     hasher.reset();
	         */
	        reset: function () {
	            // Reset data buffer
	            BufferedBlockAlgorithm.reset.call(this);

	            // Perform concrete-hasher logic
	            this._doReset();
	        },

	        /**
	         * Updates this hasher with a message.
	         *
	         * @param {WordArray|string} messageUpdate The message to append.
	         *
	         * @return {Hasher} This hasher.
	         *
	         * @example
	         *
	         *     hasher.update('message');
	         *     hasher.update(wordArray);
	         */
	        update: function (messageUpdate) {
	            // Append
	            this._append(messageUpdate);

	            // Update the hash
	            this._process();

	            // Chainable
	            return this;
	        },

	        /**
	         * Finalizes the hash computation.
	         * Note that the finalize operation is effectively a destructive, read-once operation.
	         *
	         * @param {WordArray|string} messageUpdate (Optional) A final message update.
	         *
	         * @return {WordArray} The hash.
	         *
	         * @example
	         *
	         *     var hash = hasher.finalize();
	         *     var hash = hasher.finalize('message');
	         *     var hash = hasher.finalize(wordArray);
	         */
	        finalize: function (messageUpdate) {
	            // Final message update
	            if (messageUpdate) {
	                this._append(messageUpdate);
	            }

	            // Perform concrete-hasher logic
	            var hash = this._doFinalize();

	            return hash;
	        },

	        blockSize: 512/32,

	        /**
	         * Creates a shortcut function to a hasher's object interface.
	         *
	         * @param {Hasher} hasher The hasher to create a helper for.
	         *
	         * @return {Function} The shortcut function.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var SHA256 = CryptoJS.lib.Hasher._createHelper(CryptoJS.algo.SHA256);
	         */
	        _createHelper: function (hasher) {
	            return function (message, cfg) {
	                return new hasher.init(cfg).finalize(message);
	            };
	        },

	        /**
	         * Creates a shortcut function to the HMAC's object interface.
	         *
	         * @param {Hasher} hasher The hasher to use in this HMAC helper.
	         *
	         * @return {Function} The shortcut function.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var HmacSHA256 = CryptoJS.lib.Hasher._createHmacHelper(CryptoJS.algo.SHA256);
	         */
	        _createHmacHelper: function (hasher) {
	            return function (message, key) {
	                return new C_algo.HMAC.init(hasher, key).finalize(message);
	            };
	        }
	    });

	    /**
	     * Algorithm namespace.
	     */
	    var C_algo = C.algo = {};

	    return C;
	}(Math));


	(function (undefined) {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var Base = C_lib.Base;
	    var X32WordArray = C_lib.WordArray;

	    /**
	     * x64 namespace.
	     */
	    var C_x64 = C.x64 = {};

	    /**
	     * A 64-bit word.
	     */
	    var X64Word = C_x64.Word = Base.extend({
	        /**
	         * Initializes a newly created 64-bit word.
	         *
	         * @param {number} high The high 32 bits.
	         * @param {number} low The low 32 bits.
	         *
	         * @example
	         *
	         *     var x64Word = CryptoJS.x64.Word.create(0x00010203, 0x04050607);
	         */
	        init: function (high, low) {
	            this.high = high;
	            this.low = low;
	        }

	        /**
	         * Bitwise NOTs this word.
	         *
	         * @return {X64Word} A new x64-Word object after negating.
	         *
	         * @example
	         *
	         *     var negated = x64Word.not();
	         */
	        // not: function () {
	            // var high = ~this.high;
	            // var low = ~this.low;

	            // return X64Word.create(high, low);
	        // },

	        /**
	         * Bitwise ANDs this word with the passed word.
	         *
	         * @param {X64Word} word The x64-Word to AND with this word.
	         *
	         * @return {X64Word} A new x64-Word object after ANDing.
	         *
	         * @example
	         *
	         *     var anded = x64Word.and(anotherX64Word);
	         */
	        // and: function (word) {
	            // var high = this.high & word.high;
	            // var low = this.low & word.low;

	            // return X64Word.create(high, low);
	        // },

	        /**
	         * Bitwise ORs this word with the passed word.
	         *
	         * @param {X64Word} word The x64-Word to OR with this word.
	         *
	         * @return {X64Word} A new x64-Word object after ORing.
	         *
	         * @example
	         *
	         *     var ored = x64Word.or(anotherX64Word);
	         */
	        // or: function (word) {
	            // var high = this.high | word.high;
	            // var low = this.low | word.low;

	            // return X64Word.create(high, low);
	        // },

	        /**
	         * Bitwise XORs this word with the passed word.
	         *
	         * @param {X64Word} word The x64-Word to XOR with this word.
	         *
	         * @return {X64Word} A new x64-Word object after XORing.
	         *
	         * @example
	         *
	         *     var xored = x64Word.xor(anotherX64Word);
	         */
	        // xor: function (word) {
	            // var high = this.high ^ word.high;
	            // var low = this.low ^ word.low;

	            // return X64Word.create(high, low);
	        // },

	        /**
	         * Shifts this word n bits to the left.
	         *
	         * @param {number} n The number of bits to shift.
	         *
	         * @return {X64Word} A new x64-Word object after shifting.
	         *
	         * @example
	         *
	         *     var shifted = x64Word.shiftL(25);
	         */
	        // shiftL: function (n) {
	            // if (n < 32) {
	                // var high = (this.high << n) | (this.low >>> (32 - n));
	                // var low = this.low << n;
	            // } else {
	                // var high = this.low << (n - 32);
	                // var low = 0;
	            // }

	            // return X64Word.create(high, low);
	        // },

	        /**
	         * Shifts this word n bits to the right.
	         *
	         * @param {number} n The number of bits to shift.
	         *
	         * @return {X64Word} A new x64-Word object after shifting.
	         *
	         * @example
	         *
	         *     var shifted = x64Word.shiftR(7);
	         */
	        // shiftR: function (n) {
	            // if (n < 32) {
	                // var low = (this.low >>> n) | (this.high << (32 - n));
	                // var high = this.high >>> n;
	            // } else {
	                // var low = this.high >>> (n - 32);
	                // var high = 0;
	            // }

	            // return X64Word.create(high, low);
	        // },

	        /**
	         * Rotates this word n bits to the left.
	         *
	         * @param {number} n The number of bits to rotate.
	         *
	         * @return {X64Word} A new x64-Word object after rotating.
	         *
	         * @example
	         *
	         *     var rotated = x64Word.rotL(25);
	         */
	        // rotL: function (n) {
	            // return this.shiftL(n).or(this.shiftR(64 - n));
	        // },

	        /**
	         * Rotates this word n bits to the right.
	         *
	         * @param {number} n The number of bits to rotate.
	         *
	         * @return {X64Word} A new x64-Word object after rotating.
	         *
	         * @example
	         *
	         *     var rotated = x64Word.rotR(7);
	         */
	        // rotR: function (n) {
	            // return this.shiftR(n).or(this.shiftL(64 - n));
	        // },

	        /**
	         * Adds this word with the passed word.
	         *
	         * @param {X64Word} word The x64-Word to add with this word.
	         *
	         * @return {X64Word} A new x64-Word object after adding.
	         *
	         * @example
	         *
	         *     var added = x64Word.add(anotherX64Word);
	         */
	        // add: function (word) {
	            // var low = (this.low + word.low) | 0;
	            // var carry = (low >>> 0) < (this.low >>> 0) ? 1 : 0;
	            // var high = (this.high + word.high + carry) | 0;

	            // return X64Word.create(high, low);
	        // }
	    });

	    /**
	     * An array of 64-bit words.
	     *
	     * @property {Array} words The array of CryptoJS.x64.Word objects.
	     * @property {number} sigBytes The number of significant bytes in this word array.
	     */
	    var X64WordArray = C_x64.WordArray = Base.extend({
	        /**
	         * Initializes a newly created word array.
	         *
	         * @param {Array} words (Optional) An array of CryptoJS.x64.Word objects.
	         * @param {number} sigBytes (Optional) The number of significant bytes in the words.
	         *
	         * @example
	         *
	         *     var wordArray = CryptoJS.x64.WordArray.create();
	         *
	         *     var wordArray = CryptoJS.x64.WordArray.create([
	         *         CryptoJS.x64.Word.create(0x00010203, 0x04050607),
	         *         CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f)
	         *     ]);
	         *
	         *     var wordArray = CryptoJS.x64.WordArray.create([
	         *         CryptoJS.x64.Word.create(0x00010203, 0x04050607),
	         *         CryptoJS.x64.Word.create(0x18191a1b, 0x1c1d1e1f)
	         *     ], 10);
	         */
	        init: function (words, sigBytes) {
	            words = this.words = words || [];

	            if (sigBytes != undefined) {
	                this.sigBytes = sigBytes;
	            } else {
	                this.sigBytes = words.length * 8;
	            }
	        },

	        /**
	         * Converts this 64-bit word array to a 32-bit word array.
	         *
	         * @return {CryptoJS.lib.WordArray} This word array's data as a 32-bit word array.
	         *
	         * @example
	         *
	         *     var x32WordArray = x64WordArray.toX32();
	         */
	        toX32: function () {
	            // Shortcuts
	            var x64Words = this.words;
	            var x64WordsLength = x64Words.length;

	            // Convert
	            var x32Words = [];
	            for (var i = 0; i < x64WordsLength; i++) {
	                var x64Word = x64Words[i];
	                x32Words.push(x64Word.high);
	                x32Words.push(x64Word.low);
	            }

	            return X32WordArray.create(x32Words, this.sigBytes);
	        },

	        /**
	         * Creates a copy of this word array.
	         *
	         * @return {X64WordArray} The clone.
	         *
	         * @example
	         *
	         *     var clone = x64WordArray.clone();
	         */
	        clone: function () {
	            var clone = Base.clone.call(this);

	            // Clone "words" array
	            var words = clone.words = this.words.slice(0);

	            // Clone each X64Word object
	            var wordsLength = words.length;
	            for (var i = 0; i < wordsLength; i++) {
	                words[i] = words[i].clone();
	            }

	            return clone;
	        }
	    });
	}());


	(function () {
	    // Check if typed arrays are supported
	    if (typeof ArrayBuffer != 'function') {
	        return;
	    }

	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var WordArray = C_lib.WordArray;

	    // Reference original init
	    var superInit = WordArray.init;

	    // Augment WordArray.init to handle typed arrays
	    var subInit = WordArray.init = function (typedArray) {
	        // Convert buffers to uint8
	        if (typedArray instanceof ArrayBuffer) {
	            typedArray = new Uint8Array(typedArray);
	        }

	        // Convert other array views to uint8
	        if (
	            typedArray instanceof Int8Array ||
	            (typeof Uint8ClampedArray !== "undefined" && typedArray instanceof Uint8ClampedArray) ||
	            typedArray instanceof Int16Array ||
	            typedArray instanceof Uint16Array ||
	            typedArray instanceof Int32Array ||
	            typedArray instanceof Uint32Array ||
	            typedArray instanceof Float32Array ||
	            typedArray instanceof Float64Array
	        ) {
	            typedArray = new Uint8Array(typedArray.buffer, typedArray.byteOffset, typedArray.byteLength);
	        }

	        // Handle Uint8Array
	        if (typedArray instanceof Uint8Array) {
	            // Shortcut
	            var typedArrayByteLength = typedArray.byteLength;

	            // Extract bytes
	            var words = [];
	            for (var i = 0; i < typedArrayByteLength; i++) {
	                words[i >>> 2] |= typedArray[i] << (24 - (i % 4) * 8);
	            }

	            // Initialize this word array
	            superInit.call(this, words, typedArrayByteLength);
	        } else {
	            // Else call normal init
	            superInit.apply(this, arguments);
	        }
	    };

	    subInit.prototype = WordArray;
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var WordArray = C_lib.WordArray;
	    var C_enc = C.enc;

	    /**
	     * UTF-16 BE encoding strategy.
	     */
	    var Utf16BE = C_enc.Utf16 = C_enc.Utf16BE = {
	        /**
	         * Converts a word array to a UTF-16 BE string.
	         *
	         * @param {WordArray} wordArray The word array.
	         *
	         * @return {string} The UTF-16 BE string.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var utf16String = CryptoJS.enc.Utf16.stringify(wordArray);
	         */
	        stringify: function (wordArray) {
	            // Shortcuts
	            var words = wordArray.words;
	            var sigBytes = wordArray.sigBytes;

	            // Convert
	            var utf16Chars = [];
	            for (var i = 0; i < sigBytes; i += 2) {
	                var codePoint = (words[i >>> 2] >>> (16 - (i % 4) * 8)) & 0xffff;
	                utf16Chars.push(String.fromCharCode(codePoint));
	            }

	            return utf16Chars.join('');
	        },

	        /**
	         * Converts a UTF-16 BE string to a word array.
	         *
	         * @param {string} utf16Str The UTF-16 BE string.
	         *
	         * @return {WordArray} The word array.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var wordArray = CryptoJS.enc.Utf16.parse(utf16String);
	         */
	        parse: function (utf16Str) {
	            // Shortcut
	            var utf16StrLength = utf16Str.length;

	            // Convert
	            var words = [];
	            for (var i = 0; i < utf16StrLength; i++) {
	                words[i >>> 1] |= utf16Str.charCodeAt(i) << (16 - (i % 2) * 16);
	            }

	            return WordArray.create(words, utf16StrLength * 2);
	        }
	    };

	    /**
	     * UTF-16 LE encoding strategy.
	     */
	    C_enc.Utf16LE = {
	        /**
	         * Converts a word array to a UTF-16 LE string.
	         *
	         * @param {WordArray} wordArray The word array.
	         *
	         * @return {string} The UTF-16 LE string.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var utf16Str = CryptoJS.enc.Utf16LE.stringify(wordArray);
	         */
	        stringify: function (wordArray) {
	            // Shortcuts
	            var words = wordArray.words;
	            var sigBytes = wordArray.sigBytes;

	            // Convert
	            var utf16Chars = [];
	            for (var i = 0; i < sigBytes; i += 2) {
	                var codePoint = swapEndian((words[i >>> 2] >>> (16 - (i % 4) * 8)) & 0xffff);
	                utf16Chars.push(String.fromCharCode(codePoint));
	            }

	            return utf16Chars.join('');
	        },

	        /**
	         * Converts a UTF-16 LE string to a word array.
	         *
	         * @param {string} utf16Str The UTF-16 LE string.
	         *
	         * @return {WordArray} The word array.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var wordArray = CryptoJS.enc.Utf16LE.parse(utf16Str);
	         */
	        parse: function (utf16Str) {
	            // Shortcut
	            var utf16StrLength = utf16Str.length;

	            // Convert
	            var words = [];
	            for (var i = 0; i < utf16StrLength; i++) {
	                words[i >>> 1] |= swapEndian(utf16Str.charCodeAt(i) << (16 - (i % 2) * 16));
	            }

	            return WordArray.create(words, utf16StrLength * 2);
	        }
	    };

	    function swapEndian(word) {
	        return ((word << 8) & 0xff00ff00) | ((word >>> 8) & 0x00ff00ff);
	    }
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var WordArray = C_lib.WordArray;
	    var C_enc = C.enc;

	    /**
	     * Base64 encoding strategy.
	     */
	    var Base64 = C_enc.Base64 = {
	        /**
	         * Converts a word array to a Base64 string.
	         *
	         * @param {WordArray} wordArray The word array.
	         *
	         * @return {string} The Base64 string.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var base64String = CryptoJS.enc.Base64.stringify(wordArray);
	         */
	        stringify: function (wordArray) {
	            // Shortcuts
	            var words = wordArray.words;
	            var sigBytes = wordArray.sigBytes;
	            var map = this._map;

	            // Clamp excess bits
	            wordArray.clamp();

	            // Convert
	            var base64Chars = [];
	            for (var i = 0; i < sigBytes; i += 3) {
	                var byte1 = (words[i >>> 2]       >>> (24 - (i % 4) * 8))       & 0xff;
	                var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff;
	                var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff;

	                var triplet = (byte1 << 16) | (byte2 << 8) | byte3;

	                for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) {
	                    base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f));
	                }
	            }

	            // Add padding
	            var paddingChar = map.charAt(64);
	            if (paddingChar) {
	                while (base64Chars.length % 4) {
	                    base64Chars.push(paddingChar);
	                }
	            }

	            return base64Chars.join('');
	        },

	        /**
	         * Converts a Base64 string to a word array.
	         *
	         * @param {string} base64Str The Base64 string.
	         *
	         * @return {WordArray} The word array.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var wordArray = CryptoJS.enc.Base64.parse(base64String);
	         */
	        parse: function (base64Str) {
	            // Shortcuts
	            var base64StrLength = base64Str.length;
	            var map = this._map;
	            var reverseMap = this._reverseMap;

	            if (!reverseMap) {
	                    reverseMap = this._reverseMap = [];
	                    for (var j = 0; j < map.length; j++) {
	                        reverseMap[map.charCodeAt(j)] = j;
	                    }
	            }

	            // Ignore padding
	            var paddingChar = map.charAt(64);
	            if (paddingChar) {
	                var paddingIndex = base64Str.indexOf(paddingChar);
	                if (paddingIndex !== -1) {
	                    base64StrLength = paddingIndex;
	                }
	            }

	            // Convert
	            return parseLoop(base64Str, base64StrLength, reverseMap);

	        },

	        _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/='
	    };

	    function parseLoop(base64Str, base64StrLength, reverseMap) {
	      var words = [];
	      var nBytes = 0;
	      for (var i = 0; i < base64StrLength; i++) {
	          if (i % 4) {
	              var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2);
	              var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2);
	              var bitsCombined = bits1 | bits2;
	              words[nBytes >>> 2] |= bitsCombined << (24 - (nBytes % 4) * 8);
	              nBytes++;
	          }
	      }
	      return WordArray.create(words, nBytes);
	    }
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var WordArray = C_lib.WordArray;
	    var C_enc = C.enc;

	    /**
	     * Base64url encoding strategy.
	     */
	    var Base64url = C_enc.Base64url = {
	        /**
	         * Converts a word array to a Base64url string.
	         *
	         * @param {WordArray} wordArray The word array.
	         *
	         * @param {boolean} urlSafe Whether to use url safe
	         *
	         * @return {string} The Base64url string.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var base64String = CryptoJS.enc.Base64url.stringify(wordArray);
	         */
	        stringify: function (wordArray, urlSafe) {
	            if (urlSafe === undefined) {
	                urlSafe = true
	            }
	            // Shortcuts
	            var words = wordArray.words;
	            var sigBytes = wordArray.sigBytes;
	            var map = urlSafe ? this._safe_map : this._map;

	            // Clamp excess bits
	            wordArray.clamp();

	            // Convert
	            var base64Chars = [];
	            for (var i = 0; i < sigBytes; i += 3) {
	                var byte1 = (words[i >>> 2]       >>> (24 - (i % 4) * 8))       & 0xff;
	                var byte2 = (words[(i + 1) >>> 2] >>> (24 - ((i + 1) % 4) * 8)) & 0xff;
	                var byte3 = (words[(i + 2) >>> 2] >>> (24 - ((i + 2) % 4) * 8)) & 0xff;

	                var triplet = (byte1 << 16) | (byte2 << 8) | byte3;

	                for (var j = 0; (j < 4) && (i + j * 0.75 < sigBytes); j++) {
	                    base64Chars.push(map.charAt((triplet >>> (6 * (3 - j))) & 0x3f));
	                }
	            }

	            // Add padding
	            var paddingChar = map.charAt(64);
	            if (paddingChar) {
	                while (base64Chars.length % 4) {
	                    base64Chars.push(paddingChar);
	                }
	            }

	            return base64Chars.join('');
	        },

	        /**
	         * Converts a Base64url string to a word array.
	         *
	         * @param {string} base64Str The Base64url string.
	         *
	         * @param {boolean} urlSafe Whether to use url safe
	         *
	         * @return {WordArray} The word array.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var wordArray = CryptoJS.enc.Base64url.parse(base64String);
	         */
	        parse: function (base64Str, urlSafe) {
	            if (urlSafe === undefined) {
	                urlSafe = true
	            }

	            // Shortcuts
	            var base64StrLength = base64Str.length;
	            var map = urlSafe ? this._safe_map : this._map;
	            var reverseMap = this._reverseMap;

	            if (!reverseMap) {
	                reverseMap = this._reverseMap = [];
	                for (var j = 0; j < map.length; j++) {
	                    reverseMap[map.charCodeAt(j)] = j;
	                }
	            }

	            // Ignore padding
	            var paddingChar = map.charAt(64);
	            if (paddingChar) {
	                var paddingIndex = base64Str.indexOf(paddingChar);
	                if (paddingIndex !== -1) {
	                    base64StrLength = paddingIndex;
	                }
	            }

	            // Convert
	            return parseLoop(base64Str, base64StrLength, reverseMap);

	        },

	        _map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/=',
	        _safe_map: 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_',
	    };

	    function parseLoop(base64Str, base64StrLength, reverseMap) {
	        var words = [];
	        var nBytes = 0;
	        for (var i = 0; i < base64StrLength; i++) {
	            if (i % 4) {
	                var bits1 = reverseMap[base64Str.charCodeAt(i - 1)] << ((i % 4) * 2);
	                var bits2 = reverseMap[base64Str.charCodeAt(i)] >>> (6 - (i % 4) * 2);
	                var bitsCombined = bits1 | bits2;
	                words[nBytes >>> 2] |= bitsCombined << (24 - (nBytes % 4) * 8);
	                nBytes++;
	            }
	        }
	        return WordArray.create(words, nBytes);
	    }
	}());


	(function (Math) {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var WordArray = C_lib.WordArray;
	    var Hasher = C_lib.Hasher;
	    var C_algo = C.algo;

	    // Constants table
	    var T = [];

	    // Compute constants
	    (function () {
	        for (var i = 0; i < 64; i++) {
	            T[i] = (Math.abs(Math.sin(i + 1)) * 0x100000000) | 0;
	        }
	    }());

	    /**
	     * MD5 hash algorithm.
	     */
	    var MD5 = C_algo.MD5 = Hasher.extend({
	        _doReset: function () {
	            this._hash = new WordArray.init([
	                0x67452301, 0xefcdab89,
	                0x98badcfe, 0x10325476
	            ]);
	        },

	        _doProcessBlock: function (M, offset) {
	            // Swap endian
	            for (var i = 0; i < 16; i++) {
	                // Shortcuts
	                var offset_i = offset + i;
	                var M_offset_i = M[offset_i];

	                M[offset_i] = (
	                    (((M_offset_i << 8)  | (M_offset_i >>> 24)) & 0x00ff00ff) |
	                    (((M_offset_i << 24) | (M_offset_i >>> 8))  & 0xff00ff00)
	                );
	            }

	            // Shortcuts
	            var H = this._hash.words;

	            var M_offset_0  = M[offset + 0];
	            var M_offset_1  = M[offset + 1];
	            var M_offset_2  = M[offset + 2];
	            var M_offset_3  = M[offset + 3];
	            var M_offset_4  = M[offset + 4];
	            var M_offset_5  = M[offset + 5];
	            var M_offset_6  = M[offset + 6];
	            var M_offset_7  = M[offset + 7];
	            var M_offset_8  = M[offset + 8];
	            var M_offset_9  = M[offset + 9];
	            var M_offset_10 = M[offset + 10];
	            var M_offset_11 = M[offset + 11];
	            var M_offset_12 = M[offset + 12];
	            var M_offset_13 = M[offset + 13];
	            var M_offset_14 = M[offset + 14];
	            var M_offset_15 = M[offset + 15];

	            // Working variables
	            var a = H[0];
	            var b = H[1];
	            var c = H[2];
	            var d = H[3];

	            // Computation
	            a = FF(a, b, c, d, M_offset_0,  7,  T[0]);
	            d = FF(d, a, b, c, M_offset_1,  12, T[1]);
	            c = FF(c, d, a, b, M_offset_2,  17, T[2]);
	            b = FF(b, c, d, a, M_offset_3,  22, T[3]);
	            a = FF(a, b, c, d, M_offset_4,  7,  T[4]);
	            d = FF(d, a, b, c, M_offset_5,  12, T[5]);
	            c = FF(c, d, a, b, M_offset_6,  17, T[6]);
	            b = FF(b, c, d, a, M_offset_7,  22, T[7]);
	            a = FF(a, b, c, d, M_offset_8,  7,  T[8]);
	            d = FF(d, a, b, c, M_offset_9,  12, T[9]);
	            c = FF(c, d, a, b, M_offset_10, 17, T[10]);
	            b = FF(b, c, d, a, M_offset_11, 22, T[11]);
	            a = FF(a, b, c, d, M_offset_12, 7,  T[12]);
	            d = FF(d, a, b, c, M_offset_13, 12, T[13]);
	            c = FF(c, d, a, b, M_offset_14, 17, T[14]);
	            b = FF(b, c, d, a, M_offset_15, 22, T[15]);

	            a = GG(a, b, c, d, M_offset_1,  5,  T[16]);
	            d = GG(d, a, b, c, M_offset_6,  9,  T[17]);
	            c = GG(c, d, a, b, M_offset_11, 14, T[18]);
	            b = GG(b, c, d, a, M_offset_0,  20, T[19]);
	            a = GG(a, b, c, d, M_offset_5,  5,  T[20]);
	            d = GG(d, a, b, c, M_offset_10, 9,  T[21]);
	            c = GG(c, d, a, b, M_offset_15, 14, T[22]);
	            b = GG(b, c, d, a, M_offset_4,  20, T[23]);
	            a = GG(a, b, c, d, M_offset_9,  5,  T[24]);
	            d = GG(d, a, b, c, M_offset_14, 9,  T[25]);
	            c = GG(c, d, a, b, M_offset_3,  14, T[26]);
	            b = GG(b, c, d, a, M_offset_8,  20, T[27]);
	            a = GG(a, b, c, d, M_offset_13, 5,  T[28]);
	            d = GG(d, a, b, c, M_offset_2,  9,  T[29]);
	            c = GG(c, d, a, b, M_offset_7,  14, T[30]);
	            b = GG(b, c, d, a, M_offset_12, 20, T[31]);

	            a = HH(a, b, c, d, M_offset_5,  4,  T[32]);
	            d = HH(d, a, b, c, M_offset_8,  11, T[33]);
	            c = HH(c, d, a, b, M_offset_11, 16, T[34]);
	            b = HH(b, c, d, a, M_offset_14, 23, T[35]);
	            a = HH(a, b, c, d, M_offset_1,  4,  T[36]);
	            d = HH(d, a, b, c, M_offset_4,  11, T[37]);
	            c = HH(c, d, a, b, M_offset_7,  16, T[38]);
	            b = HH(b, c, d, a, M_offset_10, 23, T[39]);
	            a = HH(a, b, c, d, M_offset_13, 4,  T[40]);
	            d = HH(d, a, b, c, M_offset_0,  11, T[41]);
	            c = HH(c, d, a, b, M_offset_3,  16, T[42]);
	            b = HH(b, c, d, a, M_offset_6,  23, T[43]);
	            a = HH(a, b, c, d, M_offset_9,  4,  T[44]);
	            d = HH(d, a, b, c, M_offset_12, 11, T[45]);
	            c = HH(c, d, a, b, M_offset_15, 16, T[46]);
	            b = HH(b, c, d, a, M_offset_2,  23, T[47]);

	            a = II(a, b, c, d, M_offset_0,  6,  T[48]);
	            d = II(d, a, b, c, M_offset_7,  10, T[49]);
	            c = II(c, d, a, b, M_offset_14, 15, T[50]);
	            b = II(b, c, d, a, M_offset_5,  21, T[51]);
	            a = II(a, b, c, d, M_offset_12, 6,  T[52]);
	            d = II(d, a, b, c, M_offset_3,  10, T[53]);
	            c = II(c, d, a, b, M_offset_10, 15, T[54]);
	            b = II(b, c, d, a, M_offset_1,  21, T[55]);
	            a = II(a, b, c, d, M_offset_8,  6,  T[56]);
	            d = II(d, a, b, c, M_offset_15, 10, T[57]);
	            c = II(c, d, a, b, M_offset_6,  15, T[58]);
	            b = II(b, c, d, a, M_offset_13, 21, T[59]);
	            a = II(a, b, c, d, M_offset_4,  6,  T[60]);
	            d = II(d, a, b, c, M_offset_11, 10, T[61]);
	            c = II(c, d, a, b, M_offset_2,  15, T[62]);
	            b = II(b, c, d, a, M_offset_9,  21, T[63]);

	            // Intermediate hash value
	            H[0] = (H[0] + a) | 0;
	            H[1] = (H[1] + b) | 0;
	            H[2] = (H[2] + c) | 0;
	            H[3] = (H[3] + d) | 0;
	        },

	        _doFinalize: function () {
	            // Shortcuts
	            var data = this._data;
	            var dataWords = data.words;

	            var nBitsTotal = this._nDataBytes * 8;
	            var nBitsLeft = data.sigBytes * 8;

	            // Add padding
	            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);

	            var nBitsTotalH = Math.floor(nBitsTotal / 0x100000000);
	            var nBitsTotalL = nBitsTotal;
	            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = (
	                (((nBitsTotalH << 8)  | (nBitsTotalH >>> 24)) & 0x00ff00ff) |
	                (((nBitsTotalH << 24) | (nBitsTotalH >>> 8))  & 0xff00ff00)
	            );
	            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = (
	                (((nBitsTotalL << 8)  | (nBitsTotalL >>> 24)) & 0x00ff00ff) |
	                (((nBitsTotalL << 24) | (nBitsTotalL >>> 8))  & 0xff00ff00)
	            );

	            data.sigBytes = (dataWords.length + 1) * 4;

	            // Hash final blocks
	            this._process();

	            // Shortcuts
	            var hash = this._hash;
	            var H = hash.words;

	            // Swap endian
	            for (var i = 0; i < 4; i++) {
	                // Shortcut
	                var H_i = H[i];

	                H[i] = (((H_i << 8)  | (H_i >>> 24)) & 0x00ff00ff) |
	                       (((H_i << 24) | (H_i >>> 8))  & 0xff00ff00);
	            }

	            // Return final computed hash
	            return hash;
	        },

	        clone: function () {
	            var clone = Hasher.clone.call(this);
	            clone._hash = this._hash.clone();

	            return clone;
	        }
	    });

	    function FF(a, b, c, d, x, s, t) {
	        var n = a + ((b & c) | (~b & d)) + x + t;
	        return ((n << s) | (n >>> (32 - s))) + b;
	    }

	    function GG(a, b, c, d, x, s, t) {
	        var n = a + ((b & d) | (c & ~d)) + x + t;
	        return ((n << s) | (n >>> (32 - s))) + b;
	    }

	    function HH(a, b, c, d, x, s, t) {
	        var n = a + (b ^ c ^ d) + x + t;
	        return ((n << s) | (n >>> (32 - s))) + b;
	    }

	    function II(a, b, c, d, x, s, t) {
	        var n = a + (c ^ (b | ~d)) + x + t;
	        return ((n << s) | (n >>> (32 - s))) + b;
	    }

	    /**
	     * Shortcut function to the hasher's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     *
	     * @return {WordArray} The hash.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hash = CryptoJS.MD5('message');
	     *     var hash = CryptoJS.MD5(wordArray);
	     */
	    C.MD5 = Hasher._createHelper(MD5);

	    /**
	     * Shortcut function to the HMAC's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     * @param {WordArray|string} key The secret key.
	     *
	     * @return {WordArray} The HMAC.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hmac = CryptoJS.HmacMD5(message, key);
	     */
	    C.HmacMD5 = Hasher._createHmacHelper(MD5);
	}(Math));


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var WordArray = C_lib.WordArray;
	    var Hasher = C_lib.Hasher;
	    var C_algo = C.algo;

	    // Reusable object
	    var W = [];

	    /**
	     * SHA-1 hash algorithm.
	     */
	    var SHA1 = C_algo.SHA1 = Hasher.extend({
	        _doReset: function () {
	            this._hash = new WordArray.init([
	                0x67452301, 0xefcdab89,
	                0x98badcfe, 0x10325476,
	                0xc3d2e1f0
	            ]);
	        },

	        _doProcessBlock: function (M, offset) {
	            // Shortcut
	            var H = this._hash.words;

	            // Working variables
	            var a = H[0];
	            var b = H[1];
	            var c = H[2];
	            var d = H[3];
	            var e = H[4];

	            // Computation
	            for (var i = 0; i < 80; i++) {
	                if (i < 16) {
	                    W[i] = M[offset + i] | 0;
	                } else {
	                    var n = W[i - 3] ^ W[i - 8] ^ W[i - 14] ^ W[i - 16];
	                    W[i] = (n << 1) | (n >>> 31);
	                }

	                var t = ((a << 5) | (a >>> 27)) + e + W[i];
	                if (i < 20) {
	                    t += ((b & c) | (~b & d)) + 0x5a827999;
	                } else if (i < 40) {
	                    t += (b ^ c ^ d) + 0x6ed9eba1;
	                } else if (i < 60) {
	                    t += ((b & c) | (b & d) | (c & d)) - 0x70e44324;
	                } else /* if (i < 80) */ {
	                    t += (b ^ c ^ d) - 0x359d3e2a;
	                }

	                e = d;
	                d = c;
	                c = (b << 30) | (b >>> 2);
	                b = a;
	                a = t;
	            }

	            // Intermediate hash value
	            H[0] = (H[0] + a) | 0;
	            H[1] = (H[1] + b) | 0;
	            H[2] = (H[2] + c) | 0;
	            H[3] = (H[3] + d) | 0;
	            H[4] = (H[4] + e) | 0;
	        },

	        _doFinalize: function () {
	            // Shortcuts
	            var data = this._data;
	            var dataWords = data.words;

	            var nBitsTotal = this._nDataBytes * 8;
	            var nBitsLeft = data.sigBytes * 8;

	            // Add padding
	            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
	            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000);
	            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal;
	            data.sigBytes = dataWords.length * 4;

	            // Hash final blocks
	            this._process();

	            // Return final computed hash
	            return this._hash;
	        },

	        clone: function () {
	            var clone = Hasher.clone.call(this);
	            clone._hash = this._hash.clone();

	            return clone;
	        }
	    });

	    /**
	     * Shortcut function to the hasher's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     *
	     * @return {WordArray} The hash.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hash = CryptoJS.SHA1('message');
	     *     var hash = CryptoJS.SHA1(wordArray);
	     */
	    C.SHA1 = Hasher._createHelper(SHA1);

	    /**
	     * Shortcut function to the HMAC's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     * @param {WordArray|string} key The secret key.
	     *
	     * @return {WordArray} The HMAC.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hmac = CryptoJS.HmacSHA1(message, key);
	     */
	    C.HmacSHA1 = Hasher._createHmacHelper(SHA1);
	}());


	(function (Math) {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var WordArray = C_lib.WordArray;
	    var Hasher = C_lib.Hasher;
	    var C_algo = C.algo;

	    // Initialization and round constants tables
	    var H = [];
	    var K = [];

	    // Compute constants
	    (function () {
	        function isPrime(n) {
	            var sqrtN = Math.sqrt(n);
	            for (var factor = 2; factor <= sqrtN; factor++) {
	                if (!(n % factor)) {
	                    return false;
	                }
	            }

	            return true;
	        }

	        function getFractionalBits(n) {
	            return ((n - (n | 0)) * 0x100000000) | 0;
	        }

	        var n = 2;
	        var nPrime = 0;
	        while (nPrime < 64) {
	            if (isPrime(n)) {
	                if (nPrime < 8) {
	                    H[nPrime] = getFractionalBits(Math.pow(n, 1 / 2));
	                }
	                K[nPrime] = getFractionalBits(Math.pow(n, 1 / 3));

	                nPrime++;
	            }

	            n++;
	        }
	    }());

	    // Reusable object
	    var W = [];

	    /**
	     * SHA-256 hash algorithm.
	     */
	    var SHA256 = C_algo.SHA256 = Hasher.extend({
	        _doReset: function () {
	            this._hash = new WordArray.init(H.slice(0));
	        },

	        _doProcessBlock: function (M, offset) {
	            // Shortcut
	            var H = this._hash.words;

	            // Working variables
	            var a = H[0];
	            var b = H[1];
	            var c = H[2];
	            var d = H[3];
	            var e = H[4];
	            var f = H[5];
	            var g = H[6];
	            var h = H[7];

	            // Computation
	            for (var i = 0; i < 64; i++) {
	                if (i < 16) {
	                    W[i] = M[offset + i] | 0;
	                } else {
	                    var gamma0x = W[i - 15];
	                    var gamma0  = ((gamma0x << 25) | (gamma0x >>> 7))  ^
	                                  ((gamma0x << 14) | (gamma0x >>> 18)) ^
	                                   (gamma0x >>> 3);

	                    var gamma1x = W[i - 2];
	                    var gamma1  = ((gamma1x << 15) | (gamma1x >>> 17)) ^
	                                  ((gamma1x << 13) | (gamma1x >>> 19)) ^
	                                   (gamma1x >>> 10);

	                    W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16];
	                }

	                var ch  = (e & f) ^ (~e & g);
	                var maj = (a & b) ^ (a & c) ^ (b & c);

	                var sigma0 = ((a << 30) | (a >>> 2)) ^ ((a << 19) | (a >>> 13)) ^ ((a << 10) | (a >>> 22));
	                var sigma1 = ((e << 26) | (e >>> 6)) ^ ((e << 21) | (e >>> 11)) ^ ((e << 7)  | (e >>> 25));

	                var t1 = h + sigma1 + ch + K[i] + W[i];
	                var t2 = sigma0 + maj;

	                h = g;
	                g = f;
	                f = e;
	                e = (d + t1) | 0;
	                d = c;
	                c = b;
	                b = a;
	                a = (t1 + t2) | 0;
	            }

	            // Intermediate hash value
	            H[0] = (H[0] + a) | 0;
	            H[1] = (H[1] + b) | 0;
	            H[2] = (H[2] + c) | 0;
	            H[3] = (H[3] + d) | 0;
	            H[4] = (H[4] + e) | 0;
	            H[5] = (H[5] + f) | 0;
	            H[6] = (H[6] + g) | 0;
	            H[7] = (H[7] + h) | 0;
	        },

	        _doFinalize: function () {
	            // Shortcuts
	            var data = this._data;
	            var dataWords = data.words;

	            var nBitsTotal = this._nDataBytes * 8;
	            var nBitsLeft = data.sigBytes * 8;

	            // Add padding
	            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
	            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = Math.floor(nBitsTotal / 0x100000000);
	            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 15] = nBitsTotal;
	            data.sigBytes = dataWords.length * 4;

	            // Hash final blocks
	            this._process();

	            // Return final computed hash
	            return this._hash;
	        },

	        clone: function () {
	            var clone = Hasher.clone.call(this);
	            clone._hash = this._hash.clone();

	            return clone;
	        }
	    });

	    /**
	     * Shortcut function to the hasher's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     *
	     * @return {WordArray} The hash.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hash = CryptoJS.SHA256('message');
	     *     var hash = CryptoJS.SHA256(wordArray);
	     */
	    C.SHA256 = Hasher._createHelper(SHA256);

	    /**
	     * Shortcut function to the HMAC's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     * @param {WordArray|string} key The secret key.
	     *
	     * @return {WordArray} The HMAC.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hmac = CryptoJS.HmacSHA256(message, key);
	     */
	    C.HmacSHA256 = Hasher._createHmacHelper(SHA256);
	}(Math));


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var WordArray = C_lib.WordArray;
	    var C_algo = C.algo;
	    var SHA256 = C_algo.SHA256;

	    /**
	     * SHA-224 hash algorithm.
	     */
	    var SHA224 = C_algo.SHA224 = SHA256.extend({
	        _doReset: function () {
	            this._hash = new WordArray.init([
	                0xc1059ed8, 0x367cd507, 0x3070dd17, 0xf70e5939,
	                0xffc00b31, 0x68581511, 0x64f98fa7, 0xbefa4fa4
	            ]);
	        },

	        _doFinalize: function () {
	            var hash = SHA256._doFinalize.call(this);

	            hash.sigBytes -= 4;

	            return hash;
	        }
	    });

	    /**
	     * Shortcut function to the hasher's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     *
	     * @return {WordArray} The hash.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hash = CryptoJS.SHA224('message');
	     *     var hash = CryptoJS.SHA224(wordArray);
	     */
	    C.SHA224 = SHA256._createHelper(SHA224);

	    /**
	     * Shortcut function to the HMAC's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     * @param {WordArray|string} key The secret key.
	     *
	     * @return {WordArray} The HMAC.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hmac = CryptoJS.HmacSHA224(message, key);
	     */
	    C.HmacSHA224 = SHA256._createHmacHelper(SHA224);
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var Hasher = C_lib.Hasher;
	    var C_x64 = C.x64;
	    var X64Word = C_x64.Word;
	    var X64WordArray = C_x64.WordArray;
	    var C_algo = C.algo;

	    function X64Word_create() {
	        return X64Word.create.apply(X64Word, arguments);
	    }

	    // Constants
	    var K = [
	        X64Word_create(0x428a2f98, 0xd728ae22), X64Word_create(0x71374491, 0x23ef65cd),
	        X64Word_create(0xb5c0fbcf, 0xec4d3b2f), X64Word_create(0xe9b5dba5, 0x8189dbbc),
	        X64Word_create(0x3956c25b, 0xf348b538), X64Word_create(0x59f111f1, 0xb605d019),
	        X64Word_create(0x923f82a4, 0xaf194f9b), X64Word_create(0xab1c5ed5, 0xda6d8118),
	        X64Word_create(0xd807aa98, 0xa3030242), X64Word_create(0x12835b01, 0x45706fbe),
	        X64Word_create(0x243185be, 0x4ee4b28c), X64Word_create(0x550c7dc3, 0xd5ffb4e2),
	        X64Word_create(0x72be5d74, 0xf27b896f), X64Word_create(0x80deb1fe, 0x3b1696b1),
	        X64Word_create(0x9bdc06a7, 0x25c71235), X64Word_create(0xc19bf174, 0xcf692694),
	        X64Word_create(0xe49b69c1, 0x9ef14ad2), X64Word_create(0xefbe4786, 0x384f25e3),
	        X64Word_create(0x0fc19dc6, 0x8b8cd5b5), X64Word_create(0x240ca1cc, 0x77ac9c65),
	        X64Word_create(0x2de92c6f, 0x592b0275), X64Word_create(0x4a7484aa, 0x6ea6e483),
	        X64Word_create(0x5cb0a9dc, 0xbd41fbd4), X64Word_create(0x76f988da, 0x831153b5),
	        X64Word_create(0x983e5152, 0xee66dfab), X64Word_create(0xa831c66d, 0x2db43210),
	        X64Word_create(0xb00327c8, 0x98fb213f), X64Word_create(0xbf597fc7, 0xbeef0ee4),
	        X64Word_create(0xc6e00bf3, 0x3da88fc2), X64Word_create(0xd5a79147, 0x930aa725),
	        X64Word_create(0x06ca6351, 0xe003826f), X64Word_create(0x14292967, 0x0a0e6e70),
	        X64Word_create(0x27b70a85, 0x46d22ffc), X64Word_create(0x2e1b2138, 0x5c26c926),
	        X64Word_create(0x4d2c6dfc, 0x5ac42aed), X64Word_create(0x53380d13, 0x9d95b3df),
	        X64Word_create(0x650a7354, 0x8baf63de), X64Word_create(0x766a0abb, 0x3c77b2a8),
	        X64Word_create(0x81c2c92e, 0x47edaee6), X64Word_create(0x92722c85, 0x1482353b),
	        X64Word_create(0xa2bfe8a1, 0x4cf10364), X64Word_create(0xa81a664b, 0xbc423001),
	        X64Word_create(0xc24b8b70, 0xd0f89791), X64Word_create(0xc76c51a3, 0x0654be30),
	        X64Word_create(0xd192e819, 0xd6ef5218), X64Word_create(0xd6990624, 0x5565a910),
	        X64Word_create(0xf40e3585, 0x5771202a), X64Word_create(0x106aa070, 0x32bbd1b8),
	        X64Word_create(0x19a4c116, 0xb8d2d0c8), X64Word_create(0x1e376c08, 0x5141ab53),
	        X64Word_create(0x2748774c, 0xdf8eeb99), X64Word_create(0x34b0bcb5, 0xe19b48a8),
	        X64Word_create(0x391c0cb3, 0xc5c95a63), X64Word_create(0x4ed8aa4a, 0xe3418acb),
	        X64Word_create(0x5b9cca4f, 0x7763e373), X64Word_create(0x682e6ff3, 0xd6b2b8a3),
	        X64Word_create(0x748f82ee, 0x5defb2fc), X64Word_create(0x78a5636f, 0x43172f60),
	        X64Word_create(0x84c87814, 0xa1f0ab72), X64Word_create(0x8cc70208, 0x1a6439ec),
	        X64Word_create(0x90befffa, 0x23631e28), X64Word_create(0xa4506ceb, 0xde82bde9),
	        X64Word_create(0xbef9a3f7, 0xb2c67915), X64Word_create(0xc67178f2, 0xe372532b),
	        X64Word_create(0xca273ece, 0xea26619c), X64Word_create(0xd186b8c7, 0x21c0c207),
	        X64Word_create(0xeada7dd6, 0xcde0eb1e), X64Word_create(0xf57d4f7f, 0xee6ed178),
	        X64Word_create(0x06f067aa, 0x72176fba), X64Word_create(0x0a637dc5, 0xa2c898a6),
	        X64Word_create(0x113f9804, 0xbef90dae), X64Word_create(0x1b710b35, 0x131c471b),
	        X64Word_create(0x28db77f5, 0x23047d84), X64Word_create(0x32caab7b, 0x40c72493),
	        X64Word_create(0x3c9ebe0a, 0x15c9bebc), X64Word_create(0x431d67c4, 0x9c100d4c),
	        X64Word_create(0x4cc5d4be, 0xcb3e42b6), X64Word_create(0x597f299c, 0xfc657e2a),
	        X64Word_create(0x5fcb6fab, 0x3ad6faec), X64Word_create(0x6c44198c, 0x4a475817)
	    ];

	    // Reusable objects
	    var W = [];
	    (function () {
	        for (var i = 0; i < 80; i++) {
	            W[i] = X64Word_create();
	        }
	    }());

	    /**
	     * SHA-512 hash algorithm.
	     */
	    var SHA512 = C_algo.SHA512 = Hasher.extend({
	        _doReset: function () {
	            this._hash = new X64WordArray.init([
	                new X64Word.init(0x6a09e667, 0xf3bcc908), new X64Word.init(0xbb67ae85, 0x84caa73b),
	                new X64Word.init(0x3c6ef372, 0xfe94f82b), new X64Word.init(0xa54ff53a, 0x5f1d36f1),
	                new X64Word.init(0x510e527f, 0xade682d1), new X64Word.init(0x9b05688c, 0x2b3e6c1f),
	                new X64Word.init(0x1f83d9ab, 0xfb41bd6b), new X64Word.init(0x5be0cd19, 0x137e2179)
	            ]);
	        },

	        _doProcessBlock: function (M, offset) {
	            // Shortcuts
	            var H = this._hash.words;

	            var H0 = H[0];
	            var H1 = H[1];
	            var H2 = H[2];
	            var H3 = H[3];
	            var H4 = H[4];
	            var H5 = H[5];
	            var H6 = H[6];
	            var H7 = H[7];

	            var H0h = H0.high;
	            var H0l = H0.low;
	            var H1h = H1.high;
	            var H1l = H1.low;
	            var H2h = H2.high;
	            var H2l = H2.low;
	            var H3h = H3.high;
	            var H3l = H3.low;
	            var H4h = H4.high;
	            var H4l = H4.low;
	            var H5h = H5.high;
	            var H5l = H5.low;
	            var H6h = H6.high;
	            var H6l = H6.low;
	            var H7h = H7.high;
	            var H7l = H7.low;

	            // Working variables
	            var ah = H0h;
	            var al = H0l;
	            var bh = H1h;
	            var bl = H1l;
	            var ch = H2h;
	            var cl = H2l;
	            var dh = H3h;
	            var dl = H3l;
	            var eh = H4h;
	            var el = H4l;
	            var fh = H5h;
	            var fl = H5l;
	            var gh = H6h;
	            var gl = H6l;
	            var hh = H7h;
	            var hl = H7l;

	            // Rounds
	            for (var i = 0; i < 80; i++) {
	                var Wil;
	                var Wih;

	                // Shortcut
	                var Wi = W[i];

	                // Extend message
	                if (i < 16) {
	                    Wih = Wi.high = M[offset + i * 2]     | 0;
	                    Wil = Wi.low  = M[offset + i * 2 + 1] | 0;
	                } else {
	                    // Gamma0
	                    var gamma0x  = W[i - 15];
	                    var gamma0xh = gamma0x.high;
	                    var gamma0xl = gamma0x.low;
	                    var gamma0h  = ((gamma0xh >>> 1) | (gamma0xl << 31)) ^ ((gamma0xh >>> 8) | (gamma0xl << 24)) ^ (gamma0xh >>> 7);
	                    var gamma0l  = ((gamma0xl >>> 1) | (gamma0xh << 31)) ^ ((gamma0xl >>> 8) | (gamma0xh << 24)) ^ ((gamma0xl >>> 7) | (gamma0xh << 25));

	                    // Gamma1
	                    var gamma1x  = W[i - 2];
	                    var gamma1xh = gamma1x.high;
	                    var gamma1xl = gamma1x.low;
	                    var gamma1h  = ((gamma1xh >>> 19) | (gamma1xl << 13)) ^ ((gamma1xh << 3) | (gamma1xl >>> 29)) ^ (gamma1xh >>> 6);
	                    var gamma1l  = ((gamma1xl >>> 19) | (gamma1xh << 13)) ^ ((gamma1xl << 3) | (gamma1xh >>> 29)) ^ ((gamma1xl >>> 6) | (gamma1xh << 26));

	                    // W[i] = gamma0 + W[i - 7] + gamma1 + W[i - 16]
	                    var Wi7  = W[i - 7];
	                    var Wi7h = Wi7.high;
	                    var Wi7l = Wi7.low;

	                    var Wi16  = W[i - 16];
	                    var Wi16h = Wi16.high;
	                    var Wi16l = Wi16.low;

	                    Wil = gamma0l + Wi7l;
	                    Wih = gamma0h + Wi7h + ((Wil >>> 0) < (gamma0l >>> 0) ? 1 : 0);
	                    Wil = Wil + gamma1l;
	                    Wih = Wih + gamma1h + ((Wil >>> 0) < (gamma1l >>> 0) ? 1 : 0);
	                    Wil = Wil + Wi16l;
	                    Wih = Wih + Wi16h + ((Wil >>> 0) < (Wi16l >>> 0) ? 1 : 0);

	                    Wi.high = Wih;
	                    Wi.low  = Wil;
	                }

	                var chh  = (eh & fh) ^ (~eh & gh);
	                var chl  = (el & fl) ^ (~el & gl);
	                var majh = (ah & bh) ^ (ah & ch) ^ (bh & ch);
	                var majl = (al & bl) ^ (al & cl) ^ (bl & cl);

	                var sigma0h = ((ah >>> 28) | (al << 4))  ^ ((ah << 30)  | (al >>> 2)) ^ ((ah << 25) | (al >>> 7));
	                var sigma0l = ((al >>> 28) | (ah << 4))  ^ ((al << 30)  | (ah >>> 2)) ^ ((al << 25) | (ah >>> 7));
	                var sigma1h = ((eh >>> 14) | (el << 18)) ^ ((eh >>> 18) | (el << 14)) ^ ((eh << 23) | (el >>> 9));
	                var sigma1l = ((el >>> 14) | (eh << 18)) ^ ((el >>> 18) | (eh << 14)) ^ ((el << 23) | (eh >>> 9));

	                // t1 = h + sigma1 + ch + K[i] + W[i]
	                var Ki  = K[i];
	                var Kih = Ki.high;
	                var Kil = Ki.low;

	                var t1l = hl + sigma1l;
	                var t1h = hh + sigma1h + ((t1l >>> 0) < (hl >>> 0) ? 1 : 0);
	                var t1l = t1l + chl;
	                var t1h = t1h + chh + ((t1l >>> 0) < (chl >>> 0) ? 1 : 0);
	                var t1l = t1l + Kil;
	                var t1h = t1h + Kih + ((t1l >>> 0) < (Kil >>> 0) ? 1 : 0);
	                var t1l = t1l + Wil;
	                var t1h = t1h + Wih + ((t1l >>> 0) < (Wil >>> 0) ? 1 : 0);

	                // t2 = sigma0 + maj
	                var t2l = sigma0l + majl;
	                var t2h = sigma0h + majh + ((t2l >>> 0) < (sigma0l >>> 0) ? 1 : 0);

	                // Update working variables
	                hh = gh;
	                hl = gl;
	                gh = fh;
	                gl = fl;
	                fh = eh;
	                fl = el;
	                el = (dl + t1l) | 0;
	                eh = (dh + t1h + ((el >>> 0) < (dl >>> 0) ? 1 : 0)) | 0;
	                dh = ch;
	                dl = cl;
	                ch = bh;
	                cl = bl;
	                bh = ah;
	                bl = al;
	                al = (t1l + t2l) | 0;
	                ah = (t1h + t2h + ((al >>> 0) < (t1l >>> 0) ? 1 : 0)) | 0;
	            }

	            // Intermediate hash value
	            H0l = H0.low  = (H0l + al);
	            H0.high = (H0h + ah + ((H0l >>> 0) < (al >>> 0) ? 1 : 0));
	            H1l = H1.low  = (H1l + bl);
	            H1.high = (H1h + bh + ((H1l >>> 0) < (bl >>> 0) ? 1 : 0));
	            H2l = H2.low  = (H2l + cl);
	            H2.high = (H2h + ch + ((H2l >>> 0) < (cl >>> 0) ? 1 : 0));
	            H3l = H3.low  = (H3l + dl);
	            H3.high = (H3h + dh + ((H3l >>> 0) < (dl >>> 0) ? 1 : 0));
	            H4l = H4.low  = (H4l + el);
	            H4.high = (H4h + eh + ((H4l >>> 0) < (el >>> 0) ? 1 : 0));
	            H5l = H5.low  = (H5l + fl);
	            H5.high = (H5h + fh + ((H5l >>> 0) < (fl >>> 0) ? 1 : 0));
	            H6l = H6.low  = (H6l + gl);
	            H6.high = (H6h + gh + ((H6l >>> 0) < (gl >>> 0) ? 1 : 0));
	            H7l = H7.low  = (H7l + hl);
	            H7.high = (H7h + hh + ((H7l >>> 0) < (hl >>> 0) ? 1 : 0));
	        },

	        _doFinalize: function () {
	            // Shortcuts
	            var data = this._data;
	            var dataWords = data.words;

	            var nBitsTotal = this._nDataBytes * 8;
	            var nBitsLeft = data.sigBytes * 8;

	            // Add padding
	            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
	            dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 30] = Math.floor(nBitsTotal / 0x100000000);
	            dataWords[(((nBitsLeft + 128) >>> 10) << 5) + 31] = nBitsTotal;
	            data.sigBytes = dataWords.length * 4;

	            // Hash final blocks
	            this._process();

	            // Convert hash to 32-bit word array before returning
	            var hash = this._hash.toX32();

	            // Return final computed hash
	            return hash;
	        },

	        clone: function () {
	            var clone = Hasher.clone.call(this);
	            clone._hash = this._hash.clone();

	            return clone;
	        },

	        blockSize: 1024/32
	    });

	    /**
	     * Shortcut function to the hasher's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     *
	     * @return {WordArray} The hash.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hash = CryptoJS.SHA512('message');
	     *     var hash = CryptoJS.SHA512(wordArray);
	     */
	    C.SHA512 = Hasher._createHelper(SHA512);

	    /**
	     * Shortcut function to the HMAC's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     * @param {WordArray|string} key The secret key.
	     *
	     * @return {WordArray} The HMAC.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hmac = CryptoJS.HmacSHA512(message, key);
	     */
	    C.HmacSHA512 = Hasher._createHmacHelper(SHA512);
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_x64 = C.x64;
	    var X64Word = C_x64.Word;
	    var X64WordArray = C_x64.WordArray;
	    var C_algo = C.algo;
	    var SHA512 = C_algo.SHA512;

	    /**
	     * SHA-384 hash algorithm.
	     */
	    var SHA384 = C_algo.SHA384 = SHA512.extend({
	        _doReset: function () {
	            this._hash = new X64WordArray.init([
	                new X64Word.init(0xcbbb9d5d, 0xc1059ed8), new X64Word.init(0x629a292a, 0x367cd507),
	                new X64Word.init(0x9159015a, 0x3070dd17), new X64Word.init(0x152fecd8, 0xf70e5939),
	                new X64Word.init(0x67332667, 0xffc00b31), new X64Word.init(0x8eb44a87, 0x68581511),
	                new X64Word.init(0xdb0c2e0d, 0x64f98fa7), new X64Word.init(0x47b5481d, 0xbefa4fa4)
	            ]);
	        },

	        _doFinalize: function () {
	            var hash = SHA512._doFinalize.call(this);

	            hash.sigBytes -= 16;

	            return hash;
	        }
	    });

	    /**
	     * Shortcut function to the hasher's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     *
	     * @return {WordArray} The hash.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hash = CryptoJS.SHA384('message');
	     *     var hash = CryptoJS.SHA384(wordArray);
	     */
	    C.SHA384 = SHA512._createHelper(SHA384);

	    /**
	     * Shortcut function to the HMAC's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     * @param {WordArray|string} key The secret key.
	     *
	     * @return {WordArray} The HMAC.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hmac = CryptoJS.HmacSHA384(message, key);
	     */
	    C.HmacSHA384 = SHA512._createHmacHelper(SHA384);
	}());


	(function (Math) {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var WordArray = C_lib.WordArray;
	    var Hasher = C_lib.Hasher;
	    var C_x64 = C.x64;
	    var X64Word = C_x64.Word;
	    var C_algo = C.algo;

	    // Constants tables
	    var RHO_OFFSETS = [];
	    var PI_INDEXES  = [];
	    var ROUND_CONSTANTS = [];

	    // Compute Constants
	    (function () {
	        // Compute rho offset constants
	        var x = 1, y = 0;
	        for (var t = 0; t < 24; t++) {
	            RHO_OFFSETS[x + 5 * y] = ((t + 1) * (t + 2) / 2) % 64;

	            var newX = y % 5;
	            var newY = (2 * x + 3 * y) % 5;
	            x = newX;
	            y = newY;
	        }

	        // Compute pi index constants
	        for (var x = 0; x < 5; x++) {
	            for (var y = 0; y < 5; y++) {
	                PI_INDEXES[x + 5 * y] = y + ((2 * x + 3 * y) % 5) * 5;
	            }
	        }

	        // Compute round constants
	        var LFSR = 0x01;
	        for (var i = 0; i < 24; i++) {
	            var roundConstantMsw = 0;
	            var roundConstantLsw = 0;

	            for (var j = 0; j < 7; j++) {
	                if (LFSR & 0x01) {
	                    var bitPosition = (1 << j) - 1;
	                    if (bitPosition < 32) {
	                        roundConstantLsw ^= 1 << bitPosition;
	                    } else /* if (bitPosition >= 32) */ {
	                        roundConstantMsw ^= 1 << (bitPosition - 32);
	                    }
	                }

	                // Compute next LFSR
	                if (LFSR & 0x80) {
	                    // Primitive polynomial over GF(2): x^8 + x^6 + x^5 + x^4 + 1
	                    LFSR = (LFSR << 1) ^ 0x71;
	                } else {
	                    LFSR <<= 1;
	                }
	            }

	            ROUND_CONSTANTS[i] = X64Word.create(roundConstantMsw, roundConstantLsw);
	        }
	    }());

	    // Reusable objects for temporary values
	    var T = [];
	    (function () {
	        for (var i = 0; i < 25; i++) {
	            T[i] = X64Word.create();
	        }
	    }());

	    /**
	     * SHA-3 hash algorithm.
	     */
	    var SHA3 = C_algo.SHA3 = Hasher.extend({
	        /**
	         * Configuration options.
	         *
	         * @property {number} outputLength
	         *   The desired number of bits in the output hash.
	         *   Only values permitted are: 224, 256, 384, 512.
	         *   Default: 512
	         */
	        cfg: Hasher.cfg.extend({
	            outputLength: 512
	        }),

	        _doReset: function () {
	            var state = this._state = []
	            for (var i = 0; i < 25; i++) {
	                state[i] = new X64Word.init();
	            }

	            this.blockSize = (1600 - 2 * this.cfg.outputLength) / 32;
	        },

	        _doProcessBlock: function (M, offset) {
	            // Shortcuts
	            var state = this._state;
	            var nBlockSizeLanes = this.blockSize / 2;

	            // Absorb
	            for (var i = 0; i < nBlockSizeLanes; i++) {
	                // Shortcuts
	                var M2i  = M[offset + 2 * i];
	                var M2i1 = M[offset + 2 * i + 1];

	                // Swap endian
	                M2i = (
	                    (((M2i << 8)  | (M2i >>> 24)) & 0x00ff00ff) |
	                    (((M2i << 24) | (M2i >>> 8))  & 0xff00ff00)
	                );
	                M2i1 = (
	                    (((M2i1 << 8)  | (M2i1 >>> 24)) & 0x00ff00ff) |
	                    (((M2i1 << 24) | (M2i1 >>> 8))  & 0xff00ff00)
	                );

	                // Absorb message into state
	                var lane = state[i];
	                lane.high ^= M2i1;
	                lane.low  ^= M2i;
	            }

	            // Rounds
	            for (var round = 0; round < 24; round++) {
	                // Theta
	                for (var x = 0; x < 5; x++) {
	                    // Mix column lanes
	                    var tMsw = 0, tLsw = 0;
	                    for (var y = 0; y < 5; y++) {
	                        var lane = state[x + 5 * y];
	                        tMsw ^= lane.high;
	                        tLsw ^= lane.low;
	                    }

	                    // Temporary values
	                    var Tx = T[x];
	                    Tx.high = tMsw;
	                    Tx.low  = tLsw;
	                }
	                for (var x = 0; x < 5; x++) {
	                    // Shortcuts
	                    var Tx4 = T[(x + 4) % 5];
	                    var Tx1 = T[(x + 1) % 5];
	                    var Tx1Msw = Tx1.high;
	                    var Tx1Lsw = Tx1.low;

	                    // Mix surrounding columns
	                    var tMsw = Tx4.high ^ ((Tx1Msw << 1) | (Tx1Lsw >>> 31));
	                    var tLsw = Tx4.low  ^ ((Tx1Lsw << 1) | (Tx1Msw >>> 31));
	                    for (var y = 0; y < 5; y++) {
	                        var lane = state[x + 5 * y];
	                        lane.high ^= tMsw;
	                        lane.low  ^= tLsw;
	                    }
	                }

	                // Rho Pi
	                for (var laneIndex = 1; laneIndex < 25; laneIndex++) {
	                    var tMsw;
	                    var tLsw;

	                    // Shortcuts
	                    var lane = state[laneIndex];
	                    var laneMsw = lane.high;
	                    var laneLsw = lane.low;
	                    var rhoOffset = RHO_OFFSETS[laneIndex];

	                    // Rotate lanes
	                    if (rhoOffset < 32) {
	                        tMsw = (laneMsw << rhoOffset) | (laneLsw >>> (32 - rhoOffset));
	                        tLsw = (laneLsw << rhoOffset) | (laneMsw >>> (32 - rhoOffset));
	                    } else /* if (rhoOffset >= 32) */ {
	                        tMsw = (laneLsw << (rhoOffset - 32)) | (laneMsw >>> (64 - rhoOffset));
	                        tLsw = (laneMsw << (rhoOffset - 32)) | (laneLsw >>> (64 - rhoOffset));
	                    }

	                    // Transpose lanes
	                    var TPiLane = T[PI_INDEXES[laneIndex]];
	                    TPiLane.high = tMsw;
	                    TPiLane.low  = tLsw;
	                }

	                // Rho pi at x = y = 0
	                var T0 = T[0];
	                var state0 = state[0];
	                T0.high = state0.high;
	                T0.low  = state0.low;

	                // Chi
	                for (var x = 0; x < 5; x++) {
	                    for (var y = 0; y < 5; y++) {
	                        // Shortcuts
	                        var laneIndex = x + 5 * y;
	                        var lane = state[laneIndex];
	                        var TLane = T[laneIndex];
	                        var Tx1Lane = T[((x + 1) % 5) + 5 * y];
	                        var Tx2Lane = T[((x + 2) % 5) + 5 * y];

	                        // Mix rows
	                        lane.high = TLane.high ^ (~Tx1Lane.high & Tx2Lane.high);
	                        lane.low  = TLane.low  ^ (~Tx1Lane.low  & Tx2Lane.low);
	                    }
	                }

	                // Iota
	                var lane = state[0];
	                var roundConstant = ROUND_CONSTANTS[round];
	                lane.high ^= roundConstant.high;
	                lane.low  ^= roundConstant.low;
	            }
	        },

	        _doFinalize: function () {
	            // Shortcuts
	            var data = this._data;
	            var dataWords = data.words;
	            var nBitsTotal = this._nDataBytes * 8;
	            var nBitsLeft = data.sigBytes * 8;
	            var blockSizeBits = this.blockSize * 32;

	            // Add padding
	            dataWords[nBitsLeft >>> 5] |= 0x1 << (24 - nBitsLeft % 32);
	            dataWords[((Math.ceil((nBitsLeft + 1) / blockSizeBits) * blockSizeBits) >>> 5) - 1] |= 0x80;
	            data.sigBytes = dataWords.length * 4;

	            // Hash final blocks
	            this._process();

	            // Shortcuts
	            var state = this._state;
	            var outputLengthBytes = this.cfg.outputLength / 8;
	            var outputLengthLanes = outputLengthBytes / 8;

	            // Squeeze
	            var hashWords = [];
	            for (var i = 0; i < outputLengthLanes; i++) {
	                // Shortcuts
	                var lane = state[i];
	                var laneMsw = lane.high;
	                var laneLsw = lane.low;

	                // Swap endian
	                laneMsw = (
	                    (((laneMsw << 8)  | (laneMsw >>> 24)) & 0x00ff00ff) |
	                    (((laneMsw << 24) | (laneMsw >>> 8))  & 0xff00ff00)
	                );
	                laneLsw = (
	                    (((laneLsw << 8)  | (laneLsw >>> 24)) & 0x00ff00ff) |
	                    (((laneLsw << 24) | (laneLsw >>> 8))  & 0xff00ff00)
	                );

	                // Squeeze state to retrieve hash
	                hashWords.push(laneLsw);
	                hashWords.push(laneMsw);
	            }

	            // Return final computed hash
	            return new WordArray.init(hashWords, outputLengthBytes);
	        },

	        clone: function () {
	            var clone = Hasher.clone.call(this);

	            var state = clone._state = this._state.slice(0);
	            for (var i = 0; i < 25; i++) {
	                state[i] = state[i].clone();
	            }

	            return clone;
	        }
	    });

	    /**
	     * Shortcut function to the hasher's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     *
	     * @return {WordArray} The hash.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hash = CryptoJS.SHA3('message');
	     *     var hash = CryptoJS.SHA3(wordArray);
	     */
	    C.SHA3 = Hasher._createHelper(SHA3);

	    /**
	     * Shortcut function to the HMAC's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     * @param {WordArray|string} key The secret key.
	     *
	     * @return {WordArray} The HMAC.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hmac = CryptoJS.HmacSHA3(message, key);
	     */
	    C.HmacSHA3 = Hasher._createHmacHelper(SHA3);
	}(Math));


	/** @preserve
	(c) 2012 by Cédric Mesnil. All rights reserved.

	Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:

	    - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
	    - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.

	THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
	*/

	(function (Math) {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var WordArray = C_lib.WordArray;
	    var Hasher = C_lib.Hasher;
	    var C_algo = C.algo;

	    // Constants table
	    var _zl = WordArray.create([
	        0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14, 15,
	        7,  4, 13,  1, 10,  6, 15,  3, 12,  0,  9,  5,  2, 14, 11,  8,
	        3, 10, 14,  4,  9, 15,  8,  1,  2,  7,  0,  6, 13, 11,  5, 12,
	        1,  9, 11, 10,  0,  8, 12,  4, 13,  3,  7, 15, 14,  5,  6,  2,
	        4,  0,  5,  9,  7, 12,  2, 10, 14,  1,  3,  8, 11,  6, 15, 13]);
	    var _zr = WordArray.create([
	        5, 14,  7,  0,  9,  2, 11,  4, 13,  6, 15,  8,  1, 10,  3, 12,
	        6, 11,  3,  7,  0, 13,  5, 10, 14, 15,  8, 12,  4,  9,  1,  2,
	        15,  5,  1,  3,  7, 14,  6,  9, 11,  8, 12,  2, 10,  0,  4, 13,
	        8,  6,  4,  1,  3, 11, 15,  0,  5, 12,  2, 13,  9,  7, 10, 14,
	        12, 15, 10,  4,  1,  5,  8,  7,  6,  2, 13, 14,  0,  3,  9, 11]);
	    var _sl = WordArray.create([
	         11, 14, 15, 12,  5,  8,  7,  9, 11, 13, 14, 15,  6,  7,  9,  8,
	        7, 6,   8, 13, 11,  9,  7, 15,  7, 12, 15,  9, 11,  7, 13, 12,
	        11, 13,  6,  7, 14,  9, 13, 15, 14,  8, 13,  6,  5, 12,  7,  5,
	          11, 12, 14, 15, 14, 15,  9,  8,  9, 14,  5,  6,  8,  6,  5, 12,
	        9, 15,  5, 11,  6,  8, 13, 12,  5, 12, 13, 14, 11,  8,  5,  6 ]);
	    var _sr = WordArray.create([
	        8,  9,  9, 11, 13, 15, 15,  5,  7,  7,  8, 11, 14, 14, 12,  6,
	        9, 13, 15,  7, 12,  8,  9, 11,  7,  7, 12,  7,  6, 15, 13, 11,
	        9,  7, 15, 11,  8,  6,  6, 14, 12, 13,  5, 14, 13, 13,  7,  5,
	        15,  5,  8, 11, 14, 14,  6, 14,  6,  9, 12,  9, 12,  5, 15,  8,
	        8,  5, 12,  9, 12,  5, 14,  6,  8, 13,  6,  5, 15, 13, 11, 11 ]);

	    var _hl =  WordArray.create([ 0x00000000, 0x5A827999, 0x6ED9EBA1, 0x8F1BBCDC, 0xA953FD4E]);
	    var _hr =  WordArray.create([ 0x50A28BE6, 0x5C4DD124, 0x6D703EF3, 0x7A6D76E9, 0x00000000]);

	    /**
	     * RIPEMD160 hash algorithm.
	     */
	    var RIPEMD160 = C_algo.RIPEMD160 = Hasher.extend({
	        _doReset: function () {
	            this._hash  = WordArray.create([0x67452301, 0xEFCDAB89, 0x98BADCFE, 0x10325476, 0xC3D2E1F0]);
	        },

	        _doProcessBlock: function (M, offset) {

	            // Swap endian
	            for (var i = 0; i < 16; i++) {
	                // Shortcuts
	                var offset_i = offset + i;
	                var M_offset_i = M[offset_i];

	                // Swap
	                M[offset_i] = (
	                    (((M_offset_i << 8)  | (M_offset_i >>> 24)) & 0x00ff00ff) |
	                    (((M_offset_i << 24) | (M_offset_i >>> 8))  & 0xff00ff00)
	                );
	            }
	            // Shortcut
	            var H  = this._hash.words;
	            var hl = _hl.words;
	            var hr = _hr.words;
	            var zl = _zl.words;
	            var zr = _zr.words;
	            var sl = _sl.words;
	            var sr = _sr.words;

	            // Working variables
	            var al, bl, cl, dl, el;
	            var ar, br, cr, dr, er;

	            ar = al = H[0];
	            br = bl = H[1];
	            cr = cl = H[2];
	            dr = dl = H[3];
	            er = el = H[4];
	            // Computation
	            var t;
	            for (var i = 0; i < 80; i += 1) {
	                t = (al +  M[offset+zl[i]])|0;
	                if (i<16){
		            t +=  f1(bl,cl,dl) + hl[0];
	                } else if (i<32) {
		            t +=  f2(bl,cl,dl) + hl[1];
	                } else if (i<48) {
		            t +=  f3(bl,cl,dl) + hl[2];
	                } else if (i<64) {
		            t +=  f4(bl,cl,dl) + hl[3];
	                } else {// if (i<80) {
		            t +=  f5(bl,cl,dl) + hl[4];
	                }
	                t = t|0;
	                t =  rotl(t,sl[i]);
	                t = (t+el)|0;
	                al = el;
	                el = dl;
	                dl = rotl(cl, 10);
	                cl = bl;
	                bl = t;

	                t = (ar + M[offset+zr[i]])|0;
	                if (i<16){
		            t +=  f5(br,cr,dr) + hr[0];
	                } else if (i<32) {
		            t +=  f4(br,cr,dr) + hr[1];
	                } else if (i<48) {
		            t +=  f3(br,cr,dr) + hr[2];
	                } else if (i<64) {
		            t +=  f2(br,cr,dr) + hr[3];
	                } else {// if (i<80) {
		            t +=  f1(br,cr,dr) + hr[4];
	                }
	                t = t|0;
	                t =  rotl(t,sr[i]) ;
	                t = (t+er)|0;
	                ar = er;
	                er = dr;
	                dr = rotl(cr, 10);
	                cr = br;
	                br = t;
	            }
	            // Intermediate hash value
	            t    = (H[1] + cl + dr)|0;
	            H[1] = (H[2] + dl + er)|0;
	            H[2] = (H[3] + el + ar)|0;
	            H[3] = (H[4] + al + br)|0;
	            H[4] = (H[0] + bl + cr)|0;
	            H[0] =  t;
	        },

	        _doFinalize: function () {
	            // Shortcuts
	            var data = this._data;
	            var dataWords = data.words;

	            var nBitsTotal = this._nDataBytes * 8;
	            var nBitsLeft = data.sigBytes * 8;

	            // Add padding
	            dataWords[nBitsLeft >>> 5] |= 0x80 << (24 - nBitsLeft % 32);
	            dataWords[(((nBitsLeft + 64) >>> 9) << 4) + 14] = (
	                (((nBitsTotal << 8)  | (nBitsTotal >>> 24)) & 0x00ff00ff) |
	                (((nBitsTotal << 24) | (nBitsTotal >>> 8))  & 0xff00ff00)
	            );
	            data.sigBytes = (dataWords.length + 1) * 4;

	            // Hash final blocks
	            this._process();

	            // Shortcuts
	            var hash = this._hash;
	            var H = hash.words;

	            // Swap endian
	            for (var i = 0; i < 5; i++) {
	                // Shortcut
	                var H_i = H[i];

	                // Swap
	                H[i] = (((H_i << 8)  | (H_i >>> 24)) & 0x00ff00ff) |
	                       (((H_i << 24) | (H_i >>> 8))  & 0xff00ff00);
	            }

	            // Return final computed hash
	            return hash;
	        },

	        clone: function () {
	            var clone = Hasher.clone.call(this);
	            clone._hash = this._hash.clone();

	            return clone;
	        }
	    });


	    function f1(x, y, z) {
	        return ((x) ^ (y) ^ (z));

	    }

	    function f2(x, y, z) {
	        return (((x)&(y)) | ((~x)&(z)));
	    }

	    function f3(x, y, z) {
	        return (((x) | (~(y))) ^ (z));
	    }

	    function f4(x, y, z) {
	        return (((x) & (z)) | ((y)&(~(z))));
	    }

	    function f5(x, y, z) {
	        return ((x) ^ ((y) |(~(z))));

	    }

	    function rotl(x,n) {
	        return (x<<n) | (x>>>(32-n));
	    }


	    /**
	     * Shortcut function to the hasher's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     *
	     * @return {WordArray} The hash.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hash = CryptoJS.RIPEMD160('message');
	     *     var hash = CryptoJS.RIPEMD160(wordArray);
	     */
	    C.RIPEMD160 = Hasher._createHelper(RIPEMD160);

	    /**
	     * Shortcut function to the HMAC's object interface.
	     *
	     * @param {WordArray|string} message The message to hash.
	     * @param {WordArray|string} key The secret key.
	     *
	     * @return {WordArray} The HMAC.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var hmac = CryptoJS.HmacRIPEMD160(message, key);
	     */
	    C.HmacRIPEMD160 = Hasher._createHmacHelper(RIPEMD160);
	}(Math));


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var Base = C_lib.Base;
	    var C_enc = C.enc;
	    var Utf8 = C_enc.Utf8;
	    var C_algo = C.algo;

	    /**
	     * HMAC algorithm.
	     */
	    var HMAC = C_algo.HMAC = Base.extend({
	        /**
	         * Initializes a newly created HMAC.
	         *
	         * @param {Hasher} hasher The hash algorithm to use.
	         * @param {WordArray|string} key The secret key.
	         *
	         * @example
	         *
	         *     var hmacHasher = CryptoJS.algo.HMAC.create(CryptoJS.algo.SHA256, key);
	         */
	        init: function (hasher, key) {
	            // Init hasher
	            hasher = this._hasher = new hasher.init();

	            // Convert string to WordArray, else assume WordArray already
	            if (typeof key == 'string') {
	                key = Utf8.parse(key);
	            }

	            // Shortcuts
	            var hasherBlockSize = hasher.blockSize;
	            var hasherBlockSizeBytes = hasherBlockSize * 4;

	            // Allow arbitrary length keys
	            if (key.sigBytes > hasherBlockSizeBytes) {
	                key = hasher.finalize(key);
	            }

	            // Clamp excess bits
	            key.clamp();

	            // Clone key for inner and outer pads
	            var oKey = this._oKey = key.clone();
	            var iKey = this._iKey = key.clone();

	            // Shortcuts
	            var oKeyWords = oKey.words;
	            var iKeyWords = iKey.words;

	            // XOR keys with pad constants
	            for (var i = 0; i < hasherBlockSize; i++) {
	                oKeyWords[i] ^= 0x5c5c5c5c;
	                iKeyWords[i] ^= 0x36363636;
	            }
	            oKey.sigBytes = iKey.sigBytes = hasherBlockSizeBytes;

	            // Set initial values
	            this.reset();
	        },

	        /**
	         * Resets this HMAC to its initial state.
	         *
	         * @example
	         *
	         *     hmacHasher.reset();
	         */
	        reset: function () {
	            // Shortcut
	            var hasher = this._hasher;

	            // Reset
	            hasher.reset();
	            hasher.update(this._iKey);
	        },

	        /**
	         * Updates this HMAC with a message.
	         *
	         * @param {WordArray|string} messageUpdate The message to append.
	         *
	         * @return {HMAC} This HMAC instance.
	         *
	         * @example
	         *
	         *     hmacHasher.update('message');
	         *     hmacHasher.update(wordArray);
	         */
	        update: function (messageUpdate) {
	            this._hasher.update(messageUpdate);

	            // Chainable
	            return this;
	        },

	        /**
	         * Finalizes the HMAC computation.
	         * Note that the finalize operation is effectively a destructive, read-once operation.
	         *
	         * @param {WordArray|string} messageUpdate (Optional) A final message update.
	         *
	         * @return {WordArray} The HMAC.
	         *
	         * @example
	         *
	         *     var hmac = hmacHasher.finalize();
	         *     var hmac = hmacHasher.finalize('message');
	         *     var hmac = hmacHasher.finalize(wordArray);
	         */
	        finalize: function (messageUpdate) {
	            // Shortcut
	            var hasher = this._hasher;

	            // Compute HMAC
	            var innerHash = hasher.finalize(messageUpdate);
	            hasher.reset();
	            var hmac = hasher.finalize(this._oKey.clone().concat(innerHash));

	            return hmac;
	        }
	    });
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var Base = C_lib.Base;
	    var WordArray = C_lib.WordArray;
	    var C_algo = C.algo;
	    var SHA256 = C_algo.SHA256;
	    var HMAC = C_algo.HMAC;

	    /**
	     * Password-Based Key Derivation Function 2 algorithm.
	     */
	    var PBKDF2 = C_algo.PBKDF2 = Base.extend({
	        /**
	         * Configuration options.
	         *
	         * @property {number} keySize The key size in words to generate. Default: 4 (128 bits)
	         * @property {Hasher} hasher The hasher to use. Default: SHA256
	         * @property {number} iterations The number of iterations to perform. Default: 250000
	         */
	        cfg: Base.extend({
	            keySize: 128/32,
	            hasher: SHA256,
	            iterations: 250000
	        }),

	        /**
	         * Initializes a newly created key derivation function.
	         *
	         * @param {Object} cfg (Optional) The configuration options to use for the derivation.
	         *
	         * @example
	         *
	         *     var kdf = CryptoJS.algo.PBKDF2.create();
	         *     var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8 });
	         *     var kdf = CryptoJS.algo.PBKDF2.create({ keySize: 8, iterations: 1000 });
	         */
	        init: function (cfg) {
	            this.cfg = this.cfg.extend(cfg);
	        },

	        /**
	         * Computes the Password-Based Key Derivation Function 2.
	         *
	         * @param {WordArray|string} password The password.
	         * @param {WordArray|string} salt A salt.
	         *
	         * @return {WordArray} The derived key.
	         *
	         * @example
	         *
	         *     var key = kdf.compute(password, salt);
	         */
	        compute: function (password, salt) {
	            // Shortcut
	            var cfg = this.cfg;

	            // Init HMAC
	            var hmac = HMAC.create(cfg.hasher, password);

	            // Initial values
	            var derivedKey = WordArray.create();
	            var blockIndex = WordArray.create([0x00000001]);

	            // Shortcuts
	            var derivedKeyWords = derivedKey.words;
	            var blockIndexWords = blockIndex.words;
	            var keySize = cfg.keySize;
	            var iterations = cfg.iterations;

	            // Generate key
	            while (derivedKeyWords.length < keySize) {
	                var block = hmac.update(salt).finalize(blockIndex);
	                hmac.reset();

	                // Shortcuts
	                var blockWords = block.words;
	                var blockWordsLength = blockWords.length;

	                // Iterations
	                var intermediate = block;
	                for (var i = 1; i < iterations; i++) {
	                    intermediate = hmac.finalize(intermediate);
	                    hmac.reset();

	                    // Shortcut
	                    var intermediateWords = intermediate.words;

	                    // XOR intermediate with block
	                    for (var j = 0; j < blockWordsLength; j++) {
	                        blockWords[j] ^= intermediateWords[j];
	                    }
	                }

	                derivedKey.concat(block);
	                blockIndexWords[0]++;
	            }
	            derivedKey.sigBytes = keySize * 4;

	            return derivedKey;
	        }
	    });

	    /**
	     * Computes the Password-Based Key Derivation Function 2.
	     *
	     * @param {WordArray|string} password The password.
	     * @param {WordArray|string} salt A salt.
	     * @param {Object} cfg (Optional) The configuration options to use for this computation.
	     *
	     * @return {WordArray} The derived key.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var key = CryptoJS.PBKDF2(password, salt);
	     *     var key = CryptoJS.PBKDF2(password, salt, { keySize: 8 });
	     *     var key = CryptoJS.PBKDF2(password, salt, { keySize: 8, iterations: 1000 });
	     */
	    C.PBKDF2 = function (password, salt, cfg) {
	        return PBKDF2.create(cfg).compute(password, salt);
	    };
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var Base = C_lib.Base;
	    var WordArray = C_lib.WordArray;
	    var C_algo = C.algo;
	    var MD5 = C_algo.MD5;

	    /**
	     * This key derivation function is meant to conform with EVP_BytesToKey.
	     * www.openssl.org/docs/crypto/EVP_BytesToKey.html
	     */
	    var EvpKDF = C_algo.EvpKDF = Base.extend({
	        /**
	         * Configuration options.
	         *
	         * @property {number} keySize The key size in words to generate. Default: 4 (128 bits)
	         * @property {Hasher} hasher The hash algorithm to use. Default: MD5
	         * @property {number} iterations The number of iterations to perform. Default: 1
	         */
	        cfg: Base.extend({
	            keySize: 128/32,
	            hasher: MD5,
	            iterations: 1
	        }),

	        /**
	         * Initializes a newly created key derivation function.
	         *
	         * @param {Object} cfg (Optional) The configuration options to use for the derivation.
	         *
	         * @example
	         *
	         *     var kdf = CryptoJS.algo.EvpKDF.create();
	         *     var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8 });
	         *     var kdf = CryptoJS.algo.EvpKDF.create({ keySize: 8, iterations: 1000 });
	         */
	        init: function (cfg) {
	            this.cfg = this.cfg.extend(cfg);
	        },

	        /**
	         * Derives a key from a password.
	         *
	         * @param {WordArray|string} password The password.
	         * @param {WordArray|string} salt A salt.
	         *
	         * @return {WordArray} The derived key.
	         *
	         * @example
	         *
	         *     var key = kdf.compute(password, salt);
	         */
	        compute: function (password, salt) {
	            var block;

	            // Shortcut
	            var cfg = this.cfg;

	            // Init hasher
	            var hasher = cfg.hasher.create();

	            // Initial values
	            var derivedKey = WordArray.create();

	            // Shortcuts
	            var derivedKeyWords = derivedKey.words;
	            var keySize = cfg.keySize;
	            var iterations = cfg.iterations;

	            // Generate key
	            while (derivedKeyWords.length < keySize) {
	                if (block) {
	                    hasher.update(block);
	                }
	                block = hasher.update(password).finalize(salt);
	                hasher.reset();

	                // Iterations
	                for (var i = 1; i < iterations; i++) {
	                    block = hasher.finalize(block);
	                    hasher.reset();
	                }

	                derivedKey.concat(block);
	            }
	            derivedKey.sigBytes = keySize * 4;

	            return derivedKey;
	        }
	    });

	    /**
	     * Derives a key from a password.
	     *
	     * @param {WordArray|string} password The password.
	     * @param {WordArray|string} salt A salt.
	     * @param {Object} cfg (Optional) The configuration options to use for this computation.
	     *
	     * @return {WordArray} The derived key.
	     *
	     * @static
	     *
	     * @example
	     *
	     *     var key = CryptoJS.EvpKDF(password, salt);
	     *     var key = CryptoJS.EvpKDF(password, salt, { keySize: 8 });
	     *     var key = CryptoJS.EvpKDF(password, salt, { keySize: 8, iterations: 1000 });
	     */
	    C.EvpKDF = function (password, salt, cfg) {
	        return EvpKDF.create(cfg).compute(password, salt);
	    };
	}());


	/**
	 * Cipher core components.
	 */
	CryptoJS.lib.Cipher || (function (undefined) {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var Base = C_lib.Base;
	    var WordArray = C_lib.WordArray;
	    var BufferedBlockAlgorithm = C_lib.BufferedBlockAlgorithm;
	    var C_enc = C.enc;
	    var Utf8 = C_enc.Utf8;
	    var Base64 = C_enc.Base64;
	    var C_algo = C.algo;
	    var EvpKDF = C_algo.EvpKDF;

	    /**
	     * Abstract base cipher template.
	     *
	     * @property {number} keySize This cipher's key size. Default: 4 (128 bits)
	     * @property {number} ivSize This cipher's IV size. Default: 4 (128 bits)
	     * @property {number} _ENC_XFORM_MODE A constant representing encryption mode.
	     * @property {number} _DEC_XFORM_MODE A constant representing decryption mode.
	     */
	    var Cipher = C_lib.Cipher = BufferedBlockAlgorithm.extend({
	        /**
	         * Configuration options.
	         *
	         * @property {WordArray} iv The IV to use for this operation.
	         */
	        cfg: Base.extend(),

	        /**
	         * Creates this cipher in encryption mode.
	         *
	         * @param {WordArray} key The key.
	         * @param {Object} cfg (Optional) The configuration options to use for this operation.
	         *
	         * @return {Cipher} A cipher instance.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var cipher = CryptoJS.algo.AES.createEncryptor(keyWordArray, { iv: ivWordArray });
	         */
	        createEncryptor: function (key, cfg) {
	            return this.create(this._ENC_XFORM_MODE, key, cfg);
	        },

	        /**
	         * Creates this cipher in decryption mode.
	         *
	         * @param {WordArray} key The key.
	         * @param {Object} cfg (Optional) The configuration options to use for this operation.
	         *
	         * @return {Cipher} A cipher instance.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var cipher = CryptoJS.algo.AES.createDecryptor(keyWordArray, { iv: ivWordArray });
	         */
	        createDecryptor: function (key, cfg) {
	            return this.create(this._DEC_XFORM_MODE, key, cfg);
	        },

	        /**
	         * Initializes a newly created cipher.
	         *
	         * @param {number} xformMode Either the encryption or decryption transormation mode constant.
	         * @param {WordArray} key The key.
	         * @param {Object} cfg (Optional) The configuration options to use for this operation.
	         *
	         * @example
	         *
	         *     var cipher = CryptoJS.algo.AES.create(CryptoJS.algo.AES._ENC_XFORM_MODE, keyWordArray, { iv: ivWordArray });
	         */
	        init: function (xformMode, key, cfg) {
	            // Apply config defaults
	            this.cfg = this.cfg.extend(cfg);

	            // Store transform mode and key
	            this._xformMode = xformMode;
	            this._key = key;

	            // Set initial values
	            this.reset();
	        },

	        /**
	         * Resets this cipher to its initial state.
	         *
	         * @example
	         *
	         *     cipher.reset();
	         */
	        reset: function () {
	            // Reset data buffer
	            BufferedBlockAlgorithm.reset.call(this);

	            // Perform concrete-cipher logic
	            this._doReset();
	        },

	        /**
	         * Adds data to be encrypted or decrypted.
	         *
	         * @param {WordArray|string} dataUpdate The data to encrypt or decrypt.
	         *
	         * @return {WordArray} The data after processing.
	         *
	         * @example
	         *
	         *     var encrypted = cipher.process('data');
	         *     var encrypted = cipher.process(wordArray);
	         */
	        process: function (dataUpdate) {
	            // Append
	            this._append(dataUpdate);

	            // Process available blocks
	            return this._process();
	        },

	        /**
	         * Finalizes the encryption or decryption process.
	         * Note that the finalize operation is effectively a destructive, read-once operation.
	         *
	         * @param {WordArray|string} dataUpdate The final data to encrypt or decrypt.
	         *
	         * @return {WordArray} The data after final processing.
	         *
	         * @example
	         *
	         *     var encrypted = cipher.finalize();
	         *     var encrypted = cipher.finalize('data');
	         *     var encrypted = cipher.finalize(wordArray);
	         */
	        finalize: function (dataUpdate) {
	            // Final data update
	            if (dataUpdate) {
	                this._append(dataUpdate);
	            }

	            // Perform concrete-cipher logic
	            var finalProcessedData = this._doFinalize();

	            return finalProcessedData;
	        },

	        keySize: 128/32,

	        ivSize: 128/32,

	        _ENC_XFORM_MODE: 1,

	        _DEC_XFORM_MODE: 2,

	        /**
	         * Creates shortcut functions to a cipher's object interface.
	         *
	         * @param {Cipher} cipher The cipher to create a helper for.
	         *
	         * @return {Object} An object with encrypt and decrypt shortcut functions.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var AES = CryptoJS.lib.Cipher._createHelper(CryptoJS.algo.AES);
	         */
	        _createHelper: (function () {
	            function selectCipherStrategy(key) {
	                if (typeof key == 'string') {
	                    return PasswordBasedCipher;
	                } else {
	                    return SerializableCipher;
	                }
	            }

	            return function (cipher) {
	                return {
	                    encrypt: function (message, key, cfg) {
	                        return selectCipherStrategy(key).encrypt(cipher, message, key, cfg);
	                    },

	                    decrypt: function (ciphertext, key, cfg) {
	                        return selectCipherStrategy(key).decrypt(cipher, ciphertext, key, cfg);
	                    }
	                };
	            };
	        }())
	    });

	    /**
	     * Abstract base stream cipher template.
	     *
	     * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 1 (32 bits)
	     */
	    var StreamCipher = C_lib.StreamCipher = Cipher.extend({
	        _doFinalize: function () {
	            // Process partial blocks
	            var finalProcessedBlocks = this._process(!!'flush');

	            return finalProcessedBlocks;
	        },

	        blockSize: 1
	    });

	    /**
	     * Mode namespace.
	     */
	    var C_mode = C.mode = {};

	    /**
	     * Abstract base block cipher mode template.
	     */
	    var BlockCipherMode = C_lib.BlockCipherMode = Base.extend({
	        /**
	         * Creates this mode for encryption.
	         *
	         * @param {Cipher} cipher A block cipher instance.
	         * @param {Array} iv The IV words.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var mode = CryptoJS.mode.CBC.createEncryptor(cipher, iv.words);
	         */
	        createEncryptor: function (cipher, iv) {
	            return this.Encryptor.create(cipher, iv);
	        },

	        /**
	         * Creates this mode for decryption.
	         *
	         * @param {Cipher} cipher A block cipher instance.
	         * @param {Array} iv The IV words.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var mode = CryptoJS.mode.CBC.createDecryptor(cipher, iv.words);
	         */
	        createDecryptor: function (cipher, iv) {
	            return this.Decryptor.create(cipher, iv);
	        },

	        /**
	         * Initializes a newly created mode.
	         *
	         * @param {Cipher} cipher A block cipher instance.
	         * @param {Array} iv The IV words.
	         *
	         * @example
	         *
	         *     var mode = CryptoJS.mode.CBC.Encryptor.create(cipher, iv.words);
	         */
	        init: function (cipher, iv) {
	            this._cipher = cipher;
	            this._iv = iv;
	        }
	    });

	    /**
	     * Cipher Block Chaining mode.
	     */
	    var CBC = C_mode.CBC = (function () {
	        /**
	         * Abstract base CBC mode.
	         */
	        var CBC = BlockCipherMode.extend();

	        /**
	         * CBC encryptor.
	         */
	        CBC.Encryptor = CBC.extend({
	            /**
	             * Processes the data block at offset.
	             *
	             * @param {Array} words The data words to operate on.
	             * @param {number} offset The offset where the block starts.
	             *
	             * @example
	             *
	             *     mode.processBlock(data.words, offset);
	             */
	            processBlock: function (words, offset) {
	                // Shortcuts
	                var cipher = this._cipher;
	                var blockSize = cipher.blockSize;

	                // XOR and encrypt
	                xorBlock.call(this, words, offset, blockSize);
	                cipher.encryptBlock(words, offset);

	                // Remember this block to use with next block
	                this._prevBlock = words.slice(offset, offset + blockSize);
	            }
	        });

	        /**
	         * CBC decryptor.
	         */
	        CBC.Decryptor = CBC.extend({
	            /**
	             * Processes the data block at offset.
	             *
	             * @param {Array} words The data words to operate on.
	             * @param {number} offset The offset where the block starts.
	             *
	             * @example
	             *
	             *     mode.processBlock(data.words, offset);
	             */
	            processBlock: function (words, offset) {
	                // Shortcuts
	                var cipher = this._cipher;
	                var blockSize = cipher.blockSize;

	                // Remember this block to use with next block
	                var thisBlock = words.slice(offset, offset + blockSize);

	                // Decrypt and XOR
	                cipher.decryptBlock(words, offset);
	                xorBlock.call(this, words, offset, blockSize);

	                // This block becomes the previous block
	                this._prevBlock = thisBlock;
	            }
	        });

	        function xorBlock(words, offset, blockSize) {
	            var block;

	            // Shortcut
	            var iv = this._iv;

	            // Choose mixing block
	            if (iv) {
	                block = iv;

	                // Remove IV for subsequent blocks
	                this._iv = undefined;
	            } else {
	                block = this._prevBlock;
	            }

	            // XOR blocks
	            for (var i = 0; i < blockSize; i++) {
	                words[offset + i] ^= block[i];
	            }
	        }

	        return CBC;
	    }());

	    /**
	     * Padding namespace.
	     */
	    var C_pad = C.pad = {};

	    /**
	     * PKCS #5/7 padding strategy.
	     */
	    var Pkcs7 = C_pad.Pkcs7 = {
	        /**
	         * Pads data using the algorithm defined in PKCS #5/7.
	         *
	         * @param {WordArray} data The data to pad.
	         * @param {number} blockSize The multiple that the data should be padded to.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     CryptoJS.pad.Pkcs7.pad(wordArray, 4);
	         */
	        pad: function (data, blockSize) {
	            // Shortcut
	            var blockSizeBytes = blockSize * 4;

	            // Count padding bytes
	            var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes;

	            // Create padding word
	            var paddingWord = (nPaddingBytes << 24) | (nPaddingBytes << 16) | (nPaddingBytes << 8) | nPaddingBytes;

	            // Create padding
	            var paddingWords = [];
	            for (var i = 0; i < nPaddingBytes; i += 4) {
	                paddingWords.push(paddingWord);
	            }
	            var padding = WordArray.create(paddingWords, nPaddingBytes);

	            // Add padding
	            data.concat(padding);
	        },

	        /**
	         * Unpads data that had been padded using the algorithm defined in PKCS #5/7.
	         *
	         * @param {WordArray} data The data to unpad.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     CryptoJS.pad.Pkcs7.unpad(wordArray);
	         */
	        unpad: function (data) {
	            // Get number of padding bytes from last byte
	            var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff;

	            // Remove padding
	            data.sigBytes -= nPaddingBytes;
	        }
	    };

	    /**
	     * Abstract base block cipher template.
	     *
	     * @property {number} blockSize The number of 32-bit words this cipher operates on. Default: 4 (128 bits)
	     */
	    var BlockCipher = C_lib.BlockCipher = Cipher.extend({
	        /**
	         * Configuration options.
	         *
	         * @property {Mode} mode The block mode to use. Default: CBC
	         * @property {Padding} padding The padding strategy to use. Default: Pkcs7
	         */
	        cfg: Cipher.cfg.extend({
	            mode: CBC,
	            padding: Pkcs7
	        }),

	        reset: function () {
	            var modeCreator;

	            // Reset cipher
	            Cipher.reset.call(this);

	            // Shortcuts
	            var cfg = this.cfg;
	            var iv = cfg.iv;
	            var mode = cfg.mode;

	            // Reset block mode
	            if (this._xformMode == this._ENC_XFORM_MODE) {
	                modeCreator = mode.createEncryptor;
	            } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ {
	                modeCreator = mode.createDecryptor;
	                // Keep at least one block in the buffer for unpadding
	                this._minBufferSize = 1;
	            }

	            if (this._mode && this._mode.__creator == modeCreator) {
	                this._mode.init(this, iv && iv.words);
	            } else {
	                this._mode = modeCreator.call(mode, this, iv && iv.words);
	                this._mode.__creator = modeCreator;
	            }
	        },

	        _doProcessBlock: function (words, offset) {
	            this._mode.processBlock(words, offset);
	        },

	        _doFinalize: function () {
	            var finalProcessedBlocks;

	            // Shortcut
	            var padding = this.cfg.padding;

	            // Finalize
	            if (this._xformMode == this._ENC_XFORM_MODE) {
	                // Pad data
	                padding.pad(this._data, this.blockSize);

	                // Process final blocks
	                finalProcessedBlocks = this._process(!!'flush');
	            } else /* if (this._xformMode == this._DEC_XFORM_MODE) */ {
	                // Process final blocks
	                finalProcessedBlocks = this._process(!!'flush');

	                // Unpad data
	                padding.unpad(finalProcessedBlocks);
	            }

	            return finalProcessedBlocks;
	        },

	        blockSize: 128/32
	    });

	    /**
	     * A collection of cipher parameters.
	     *
	     * @property {WordArray} ciphertext The raw ciphertext.
	     * @property {WordArray} key The key to this ciphertext.
	     * @property {WordArray} iv The IV used in the ciphering operation.
	     * @property {WordArray} salt The salt used with a key derivation function.
	     * @property {Cipher} algorithm The cipher algorithm.
	     * @property {Mode} mode The block mode used in the ciphering operation.
	     * @property {Padding} padding The padding scheme used in the ciphering operation.
	     * @property {number} blockSize The block size of the cipher.
	     * @property {Format} formatter The default formatting strategy to convert this cipher params object to a string.
	     */
	    var CipherParams = C_lib.CipherParams = Base.extend({
	        /**
	         * Initializes a newly created cipher params object.
	         *
	         * @param {Object} cipherParams An object with any of the possible cipher parameters.
	         *
	         * @example
	         *
	         *     var cipherParams = CryptoJS.lib.CipherParams.create({
	         *         ciphertext: ciphertextWordArray,
	         *         key: keyWordArray,
	         *         iv: ivWordArray,
	         *         salt: saltWordArray,
	         *         algorithm: CryptoJS.algo.AES,
	         *         mode: CryptoJS.mode.CBC,
	         *         padding: CryptoJS.pad.PKCS7,
	         *         blockSize: 4,
	         *         formatter: CryptoJS.format.OpenSSL
	         *     });
	         */
	        init: function (cipherParams) {
	            this.mixIn(cipherParams);
	        },

	        /**
	         * Converts this cipher params object to a string.
	         *
	         * @param {Format} formatter (Optional) The formatting strategy to use.
	         *
	         * @return {string} The stringified cipher params.
	         *
	         * @throws Error If neither the formatter nor the default formatter is set.
	         *
	         * @example
	         *
	         *     var string = cipherParams + '';
	         *     var string = cipherParams.toString();
	         *     var string = cipherParams.toString(CryptoJS.format.OpenSSL);
	         */
	        toString: function (formatter) {
	            return (formatter || this.formatter).stringify(this);
	        }
	    });

	    /**
	     * Format namespace.
	     */
	    var C_format = C.format = {};

	    /**
	     * OpenSSL formatting strategy.
	     */
	    var OpenSSLFormatter = C_format.OpenSSL = {
	        /**
	         * Converts a cipher params object to an OpenSSL-compatible string.
	         *
	         * @param {CipherParams} cipherParams The cipher params object.
	         *
	         * @return {string} The OpenSSL-compatible string.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var openSSLString = CryptoJS.format.OpenSSL.stringify(cipherParams);
	         */
	        stringify: function (cipherParams) {
	            var wordArray;

	            // Shortcuts
	            var ciphertext = cipherParams.ciphertext;
	            var salt = cipherParams.salt;

	            // Format
	            if (salt) {
	                wordArray = WordArray.create([0x53616c74, 0x65645f5f]).concat(salt).concat(ciphertext);
	            } else {
	                wordArray = ciphertext;
	            }

	            return wordArray.toString(Base64);
	        },

	        /**
	         * Converts an OpenSSL-compatible string to a cipher params object.
	         *
	         * @param {string} openSSLStr The OpenSSL-compatible string.
	         *
	         * @return {CipherParams} The cipher params object.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var cipherParams = CryptoJS.format.OpenSSL.parse(openSSLString);
	         */
	        parse: function (openSSLStr) {
	            var salt;

	            // Parse base64
	            var ciphertext = Base64.parse(openSSLStr);

	            // Shortcut
	            var ciphertextWords = ciphertext.words;

	            // Test for salt
	            if (ciphertextWords[0] == 0x53616c74 && ciphertextWords[1] == 0x65645f5f) {
	                // Extract salt
	                salt = WordArray.create(ciphertextWords.slice(2, 4));

	                // Remove salt from ciphertext
	                ciphertextWords.splice(0, 4);
	                ciphertext.sigBytes -= 16;
	            }

	            return CipherParams.create({ ciphertext: ciphertext, salt: salt });
	        }
	    };

	    /**
	     * A cipher wrapper that returns ciphertext as a serializable cipher params object.
	     */
	    var SerializableCipher = C_lib.SerializableCipher = Base.extend({
	        /**
	         * Configuration options.
	         *
	         * @property {Formatter} format The formatting strategy to convert cipher param objects to and from a string. Default: OpenSSL
	         */
	        cfg: Base.extend({
	            format: OpenSSLFormatter
	        }),

	        /**
	         * Encrypts a message.
	         *
	         * @param {Cipher} cipher The cipher algorithm to use.
	         * @param {WordArray|string} message The message to encrypt.
	         * @param {WordArray} key The key.
	         * @param {Object} cfg (Optional) The configuration options to use for this operation.
	         *
	         * @return {CipherParams} A cipher params object.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key);
	         *     var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv });
	         *     var ciphertextParams = CryptoJS.lib.SerializableCipher.encrypt(CryptoJS.algo.AES, message, key, { iv: iv, format: CryptoJS.format.OpenSSL });
	         */
	        encrypt: function (cipher, message, key, cfg) {
	            // Apply config defaults
	            cfg = this.cfg.extend(cfg);

	            // Encrypt
	            var encryptor = cipher.createEncryptor(key, cfg);
	            var ciphertext = encryptor.finalize(message);

	            // Shortcut
	            var cipherCfg = encryptor.cfg;

	            // Create and return serializable cipher params
	            return CipherParams.create({
	                ciphertext: ciphertext,
	                key: key,
	                iv: cipherCfg.iv,
	                algorithm: cipher,
	                mode: cipherCfg.mode,
	                padding: cipherCfg.padding,
	                blockSize: cipher.blockSize,
	                formatter: cfg.format
	            });
	        },

	        /**
	         * Decrypts serialized ciphertext.
	         *
	         * @param {Cipher} cipher The cipher algorithm to use.
	         * @param {CipherParams|string} ciphertext The ciphertext to decrypt.
	         * @param {WordArray} key The key.
	         * @param {Object} cfg (Optional) The configuration options to use for this operation.
	         *
	         * @return {WordArray} The plaintext.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, key, { iv: iv, format: CryptoJS.format.OpenSSL });
	         *     var plaintext = CryptoJS.lib.SerializableCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, key, { iv: iv, format: CryptoJS.format.OpenSSL });
	         */
	        decrypt: function (cipher, ciphertext, key, cfg) {
	            // Apply config defaults
	            cfg = this.cfg.extend(cfg);

	            // Convert string to CipherParams
	            ciphertext = this._parse(ciphertext, cfg.format);

	            // Decrypt
	            var plaintext = cipher.createDecryptor(key, cfg).finalize(ciphertext.ciphertext);

	            return plaintext;
	        },

	        /**
	         * Converts serialized ciphertext to CipherParams,
	         * else assumed CipherParams already and returns ciphertext unchanged.
	         *
	         * @param {CipherParams|string} ciphertext The ciphertext.
	         * @param {Formatter} format The formatting strategy to use to parse serialized ciphertext.
	         *
	         * @return {CipherParams} The unserialized ciphertext.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var ciphertextParams = CryptoJS.lib.SerializableCipher._parse(ciphertextStringOrParams, format);
	         */
	        _parse: function (ciphertext, format) {
	            if (typeof ciphertext == 'string') {
	                return format.parse(ciphertext, this);
	            } else {
	                return ciphertext;
	            }
	        }
	    });

	    /**
	     * Key derivation function namespace.
	     */
	    var C_kdf = C.kdf = {};

	    /**
	     * OpenSSL key derivation function.
	     */
	    var OpenSSLKdf = C_kdf.OpenSSL = {
	        /**
	         * Derives a key and IV from a password.
	         *
	         * @param {string} password The password to derive from.
	         * @param {number} keySize The size in words of the key to generate.
	         * @param {number} ivSize The size in words of the IV to generate.
	         * @param {WordArray|string} salt (Optional) A 64-bit salt to use. If omitted, a salt will be generated randomly.
	         *
	         * @return {CipherParams} A cipher params object with the key, IV, and salt.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32);
	         *     var derivedParams = CryptoJS.kdf.OpenSSL.execute('Password', 256/32, 128/32, 'saltsalt');
	         */
	        execute: function (password, keySize, ivSize, salt, hasher) {
	            // Generate random salt
	            if (!salt) {
	                salt = WordArray.random(64/8);
	            }

	            // Derive key and IV
	            if (!hasher) {
	                var key = EvpKDF.create({ keySize: keySize + ivSize }).compute(password, salt);
	            } else {
	                var key = EvpKDF.create({ keySize: keySize + ivSize, hasher: hasher }).compute(password, salt);
	            }


	            // Separate key and IV
	            var iv = WordArray.create(key.words.slice(keySize), ivSize * 4);
	            key.sigBytes = keySize * 4;

	            // Return params
	            return CipherParams.create({ key: key, iv: iv, salt: salt });
	        }
	    };

	    /**
	     * A serializable cipher wrapper that derives the key from a password,
	     * and returns ciphertext as a serializable cipher params object.
	     */
	    var PasswordBasedCipher = C_lib.PasswordBasedCipher = SerializableCipher.extend({
	        /**
	         * Configuration options.
	         *
	         * @property {KDF} kdf The key derivation function to use to generate a key and IV from a password. Default: OpenSSL
	         */
	        cfg: SerializableCipher.cfg.extend({
	            kdf: OpenSSLKdf
	        }),

	        /**
	         * Encrypts a message using a password.
	         *
	         * @param {Cipher} cipher The cipher algorithm to use.
	         * @param {WordArray|string} message The message to encrypt.
	         * @param {string} password The password.
	         * @param {Object} cfg (Optional) The configuration options to use for this operation.
	         *
	         * @return {CipherParams} A cipher params object.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password');
	         *     var ciphertextParams = CryptoJS.lib.PasswordBasedCipher.encrypt(CryptoJS.algo.AES, message, 'password', { format: CryptoJS.format.OpenSSL });
	         */
	        encrypt: function (cipher, message, password, cfg) {
	            // Apply config defaults
	            cfg = this.cfg.extend(cfg);

	            // Derive key and other params
	            var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, cfg.salt, cfg.hasher);

	            // Add IV to config
	            cfg.iv = derivedParams.iv;

	            // Encrypt
	            var ciphertext = SerializableCipher.encrypt.call(this, cipher, message, derivedParams.key, cfg);

	            // Mix in derived params
	            ciphertext.mixIn(derivedParams);

	            return ciphertext;
	        },

	        /**
	         * Decrypts serialized ciphertext using a password.
	         *
	         * @param {Cipher} cipher The cipher algorithm to use.
	         * @param {CipherParams|string} ciphertext The ciphertext to decrypt.
	         * @param {string} password The password.
	         * @param {Object} cfg (Optional) The configuration options to use for this operation.
	         *
	         * @return {WordArray} The plaintext.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, formattedCiphertext, 'password', { format: CryptoJS.format.OpenSSL });
	         *     var plaintext = CryptoJS.lib.PasswordBasedCipher.decrypt(CryptoJS.algo.AES, ciphertextParams, 'password', { format: CryptoJS.format.OpenSSL });
	         */
	        decrypt: function (cipher, ciphertext, password, cfg) {
	            // Apply config defaults
	            cfg = this.cfg.extend(cfg);

	            // Convert string to CipherParams
	            ciphertext = this._parse(ciphertext, cfg.format);

	            // Derive key and other params
	            var derivedParams = cfg.kdf.execute(password, cipher.keySize, cipher.ivSize, ciphertext.salt, cfg.hasher);

	            // Add IV to config
	            cfg.iv = derivedParams.iv;

	            // Decrypt
	            var plaintext = SerializableCipher.decrypt.call(this, cipher, ciphertext, derivedParams.key, cfg);

	            return plaintext;
	        }
	    });
	}());


	/**
	 * Cipher Feedback block mode.
	 */
	CryptoJS.mode.CFB = (function () {
	    var CFB = CryptoJS.lib.BlockCipherMode.extend();

	    CFB.Encryptor = CFB.extend({
	        processBlock: function (words, offset) {
	            // Shortcuts
	            var cipher = this._cipher;
	            var blockSize = cipher.blockSize;

	            generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher);

	            // Remember this block to use with next block
	            this._prevBlock = words.slice(offset, offset + blockSize);
	        }
	    });

	    CFB.Decryptor = CFB.extend({
	        processBlock: function (words, offset) {
	            // Shortcuts
	            var cipher = this._cipher;
	            var blockSize = cipher.blockSize;

	            // Remember this block to use with next block
	            var thisBlock = words.slice(offset, offset + blockSize);

	            generateKeystreamAndEncrypt.call(this, words, offset, blockSize, cipher);

	            // This block becomes the previous block
	            this._prevBlock = thisBlock;
	        }
	    });

	    function generateKeystreamAndEncrypt(words, offset, blockSize, cipher) {
	        var keystream;

	        // Shortcut
	        var iv = this._iv;

	        // Generate keystream
	        if (iv) {
	            keystream = iv.slice(0);

	            // Remove IV for subsequent blocks
	            this._iv = undefined;
	        } else {
	            keystream = this._prevBlock;
	        }
	        cipher.encryptBlock(keystream, 0);

	        // Encrypt
	        for (var i = 0; i < blockSize; i++) {
	            words[offset + i] ^= keystream[i];
	        }
	    }

	    return CFB;
	}());


	/**
	 * Counter block mode.
	 */
	CryptoJS.mode.CTR = (function () {
	    var CTR = CryptoJS.lib.BlockCipherMode.extend();

	    var Encryptor = CTR.Encryptor = CTR.extend({
	        processBlock: function (words, offset) {
	            // Shortcuts
	            var cipher = this._cipher
	            var blockSize = cipher.blockSize;
	            var iv = this._iv;
	            var counter = this._counter;

	            // Generate keystream
	            if (iv) {
	                counter = this._counter = iv.slice(0);

	                // Remove IV for subsequent blocks
	                this._iv = undefined;
	            }
	            var keystream = counter.slice(0);
	            cipher.encryptBlock(keystream, 0);

	            // Increment counter
	            counter[blockSize - 1] = (counter[blockSize - 1] + 1) | 0

	            // Encrypt
	            for (var i = 0; i < blockSize; i++) {
	                words[offset + i] ^= keystream[i];
	            }
	        }
	    });

	    CTR.Decryptor = Encryptor;

	    return CTR;
	}());


	/** @preserve
	 * Counter block mode compatible with  Dr Brian Gladman fileenc.c
	 * derived from CryptoJS.mode.CTR
	 * Jan Hruby jhruby.web@gmail.com
	 */
	CryptoJS.mode.CTRGladman = (function () {
	    var CTRGladman = CryptoJS.lib.BlockCipherMode.extend();

		function incWord(word)
		{
			if (((word >> 24) & 0xff) === 0xff) { //overflow
			var b1 = (word >> 16)&0xff;
			var b2 = (word >> 8)&0xff;
			var b3 = word & 0xff;

			if (b1 === 0xff) // overflow b1
			{
			b1 = 0;
			if (b2 === 0xff)
			{
				b2 = 0;
				if (b3 === 0xff)
				{
					b3 = 0;
				}
				else
				{
					++b3;
				}
			}
			else
			{
				++b2;
			}
			}
			else
			{
			++b1;
			}

			word = 0;
			word += (b1 << 16);
			word += (b2 << 8);
			word += b3;
			}
			else
			{
			word += (0x01 << 24);
			}
			return word;
		}

		function incCounter(counter)
		{
			if ((counter[0] = incWord(counter[0])) === 0)
			{
				// encr_data in fileenc.c from  Dr Brian Gladman's counts only with DWORD j < 8
				counter[1] = incWord(counter[1]);
			}
			return counter;
		}

	    var Encryptor = CTRGladman.Encryptor = CTRGladman.extend({
	        processBlock: function (words, offset) {
	            // Shortcuts
	            var cipher = this._cipher
	            var blockSize = cipher.blockSize;
	            var iv = this._iv;
	            var counter = this._counter;

	            // Generate keystream
	            if (iv) {
	                counter = this._counter = iv.slice(0);

	                // Remove IV for subsequent blocks
	                this._iv = undefined;
	            }

				incCounter(counter);

				var keystream = counter.slice(0);
	            cipher.encryptBlock(keystream, 0);

	            // Encrypt
	            for (var i = 0; i < blockSize; i++) {
	                words[offset + i] ^= keystream[i];
	            }
	        }
	    });

	    CTRGladman.Decryptor = Encryptor;

	    return CTRGladman;
	}());




	/**
	 * Output Feedback block mode.
	 */
	CryptoJS.mode.OFB = (function () {
	    var OFB = CryptoJS.lib.BlockCipherMode.extend();

	    var Encryptor = OFB.Encryptor = OFB.extend({
	        processBlock: function (words, offset) {
	            // Shortcuts
	            var cipher = this._cipher
	            var blockSize = cipher.blockSize;
	            var iv = this._iv;
	            var keystream = this._keystream;

	            // Generate keystream
	            if (iv) {
	                keystream = this._keystream = iv.slice(0);

	                // Remove IV for subsequent blocks
	                this._iv = undefined;
	            }
	            cipher.encryptBlock(keystream, 0);

	            // Encrypt
	            for (var i = 0; i < blockSize; i++) {
	                words[offset + i] ^= keystream[i];
	            }
	        }
	    });

	    OFB.Decryptor = Encryptor;

	    return OFB;
	}());


	/**
	 * Electronic Codebook block mode.
	 */
	CryptoJS.mode.ECB = (function () {
	    var ECB = CryptoJS.lib.BlockCipherMode.extend();

	    ECB.Encryptor = ECB.extend({
	        processBlock: function (words, offset) {
	            this._cipher.encryptBlock(words, offset);
	        }
	    });

	    ECB.Decryptor = ECB.extend({
	        processBlock: function (words, offset) {
	            this._cipher.decryptBlock(words, offset);
	        }
	    });

	    return ECB;
	}());


	/**
	 * ANSI X.923 padding strategy.
	 */
	CryptoJS.pad.AnsiX923 = {
	    pad: function (data, blockSize) {
	        // Shortcuts
	        var dataSigBytes = data.sigBytes;
	        var blockSizeBytes = blockSize * 4;

	        // Count padding bytes
	        var nPaddingBytes = blockSizeBytes - dataSigBytes % blockSizeBytes;

	        // Compute last byte position
	        var lastBytePos = dataSigBytes + nPaddingBytes - 1;

	        // Pad
	        data.clamp();
	        data.words[lastBytePos >>> 2] |= nPaddingBytes << (24 - (lastBytePos % 4) * 8);
	        data.sigBytes += nPaddingBytes;
	    },

	    unpad: function (data) {
	        // Get number of padding bytes from last byte
	        var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff;

	        // Remove padding
	        data.sigBytes -= nPaddingBytes;
	    }
	};


	/**
	 * ISO 10126 padding strategy.
	 */
	CryptoJS.pad.Iso10126 = {
	    pad: function (data, blockSize) {
	        // Shortcut
	        var blockSizeBytes = blockSize * 4;

	        // Count padding bytes
	        var nPaddingBytes = blockSizeBytes - data.sigBytes % blockSizeBytes;

	        // Pad
	        data.concat(CryptoJS.lib.WordArray.random(nPaddingBytes - 1)).
	             concat(CryptoJS.lib.WordArray.create([nPaddingBytes << 24], 1));
	    },

	    unpad: function (data) {
	        // Get number of padding bytes from last byte
	        var nPaddingBytes = data.words[(data.sigBytes - 1) >>> 2] & 0xff;

	        // Remove padding
	        data.sigBytes -= nPaddingBytes;
	    }
	};


	/**
	 * ISO/IEC 9797-1 Padding Method 2.
	 */
	CryptoJS.pad.Iso97971 = {
	    pad: function (data, blockSize) {
	        // Add 0x80 byte
	        data.concat(CryptoJS.lib.WordArray.create([0x80000000], 1));

	        // Zero pad the rest
	        CryptoJS.pad.ZeroPadding.pad(data, blockSize);
	    },

	    unpad: function (data) {
	        // Remove zero padding
	        CryptoJS.pad.ZeroPadding.unpad(data);

	        // Remove one more byte -- the 0x80 byte
	        data.sigBytes--;
	    }
	};


	/**
	 * Zero padding strategy.
	 */
	CryptoJS.pad.ZeroPadding = {
	    pad: function (data, blockSize) {
	        // Shortcut
	        var blockSizeBytes = blockSize * 4;

	        // Pad
	        data.clamp();
	        data.sigBytes += blockSizeBytes - ((data.sigBytes % blockSizeBytes) || blockSizeBytes);
	    },

	    unpad: function (data) {
	        // Shortcut
	        var dataWords = data.words;

	        // Unpad
	        var i = data.sigBytes - 1;
	        for (var i = data.sigBytes - 1; i >= 0; i--) {
	            if (((dataWords[i >>> 2] >>> (24 - (i % 4) * 8)) & 0xff)) {
	                data.sigBytes = i + 1;
	                break;
	            }
	        }
	    }
	};


	/**
	 * A noop padding strategy.
	 */
	CryptoJS.pad.NoPadding = {
	    pad: function () {
	    },

	    unpad: function () {
	    }
	};


	(function (undefined) {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var CipherParams = C_lib.CipherParams;
	    var C_enc = C.enc;
	    var Hex = C_enc.Hex;
	    var C_format = C.format;

	    var HexFormatter = C_format.Hex = {
	        /**
	         * Converts the ciphertext of a cipher params object to a hexadecimally encoded string.
	         *
	         * @param {CipherParams} cipherParams The cipher params object.
	         *
	         * @return {string} The hexadecimally encoded string.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var hexString = CryptoJS.format.Hex.stringify(cipherParams);
	         */
	        stringify: function (cipherParams) {
	            return cipherParams.ciphertext.toString(Hex);
	        },

	        /**
	         * Converts a hexadecimally encoded ciphertext string to a cipher params object.
	         *
	         * @param {string} input The hexadecimally encoded string.
	         *
	         * @return {CipherParams} The cipher params object.
	         *
	         * @static
	         *
	         * @example
	         *
	         *     var cipherParams = CryptoJS.format.Hex.parse(hexString);
	         */
	        parse: function (input) {
	            var ciphertext = Hex.parse(input);
	            return CipherParams.create({ ciphertext: ciphertext });
	        }
	    };
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var BlockCipher = C_lib.BlockCipher;
	    var C_algo = C.algo;

	    // Lookup tables
	    var SBOX = [];
	    var INV_SBOX = [];
	    var SUB_MIX_0 = [];
	    var SUB_MIX_1 = [];
	    var SUB_MIX_2 = [];
	    var SUB_MIX_3 = [];
	    var INV_SUB_MIX_0 = [];
	    var INV_SUB_MIX_1 = [];
	    var INV_SUB_MIX_2 = [];
	    var INV_SUB_MIX_3 = [];

	    // Compute lookup tables
	    (function () {
	        // Compute double table
	        var d = [];
	        for (var i = 0; i < 256; i++) {
	            if (i < 128) {
	                d[i] = i << 1;
	            } else {
	                d[i] = (i << 1) ^ 0x11b;
	            }
	        }

	        // Walk GF(2^8)
	        var x = 0;
	        var xi = 0;
	        for (var i = 0; i < 256; i++) {
	            // Compute sbox
	            var sx = xi ^ (xi << 1) ^ (xi << 2) ^ (xi << 3) ^ (xi << 4);
	            sx = (sx >>> 8) ^ (sx & 0xff) ^ 0x63;
	            SBOX[x] = sx;
	            INV_SBOX[sx] = x;

	            // Compute multiplication
	            var x2 = d[x];
	            var x4 = d[x2];
	            var x8 = d[x4];

	            // Compute sub bytes, mix columns tables
	            var t = (d[sx] * 0x101) ^ (sx * 0x1010100);
	            SUB_MIX_0[x] = (t << 24) | (t >>> 8);
	            SUB_MIX_1[x] = (t << 16) | (t >>> 16);
	            SUB_MIX_2[x] = (t << 8)  | (t >>> 24);
	            SUB_MIX_3[x] = t;

	            // Compute inv sub bytes, inv mix columns tables
	            var t = (x8 * 0x1010101) ^ (x4 * 0x10001) ^ (x2 * 0x101) ^ (x * 0x1010100);
	            INV_SUB_MIX_0[sx] = (t << 24) | (t >>> 8);
	            INV_SUB_MIX_1[sx] = (t << 16) | (t >>> 16);
	            INV_SUB_MIX_2[sx] = (t << 8)  | (t >>> 24);
	            INV_SUB_MIX_3[sx] = t;

	            // Compute next counter
	            if (!x) {
	                x = xi = 1;
	            } else {
	                x = x2 ^ d[d[d[x8 ^ x2]]];
	                xi ^= d[d[xi]];
	            }
	        }
	    }());

	    // Precomputed Rcon lookup
	    var RCON = [0x00, 0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80, 0x1b, 0x36];

	    /**
	     * AES block cipher algorithm.
	     */
	    var AES = C_algo.AES = BlockCipher.extend({
	        _doReset: function () {
	            var t;

	            // Skip reset of nRounds has been set before and key did not change
	            if (this._nRounds && this._keyPriorReset === this._key) {
	                return;
	            }

	            // Shortcuts
	            var key = this._keyPriorReset = this._key;
	            var keyWords = key.words;
	            var keySize = key.sigBytes / 4;

	            // Compute number of rounds
	            var nRounds = this._nRounds = keySize + 6;

	            // Compute number of key schedule rows
	            var ksRows = (nRounds + 1) * 4;

	            // Compute key schedule
	            var keySchedule = this._keySchedule = [];
	            for (var ksRow = 0; ksRow < ksRows; ksRow++) {
	                if (ksRow < keySize) {
	                    keySchedule[ksRow] = keyWords[ksRow];
	                } else {
	                    t = keySchedule[ksRow - 1];

	                    if (!(ksRow % keySize)) {
	                        // Rot word
	                        t = (t << 8) | (t >>> 24);

	                        // Sub word
	                        t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff];

	                        // Mix Rcon
	                        t ^= RCON[(ksRow / keySize) | 0] << 24;
	                    } else if (keySize > 6 && ksRow % keySize == 4) {
	                        // Sub word
	                        t = (SBOX[t >>> 24] << 24) | (SBOX[(t >>> 16) & 0xff] << 16) | (SBOX[(t >>> 8) & 0xff] << 8) | SBOX[t & 0xff];
	                    }

	                    keySchedule[ksRow] = keySchedule[ksRow - keySize] ^ t;
	                }
	            }

	            // Compute inv key schedule
	            var invKeySchedule = this._invKeySchedule = [];
	            for (var invKsRow = 0; invKsRow < ksRows; invKsRow++) {
	                var ksRow = ksRows - invKsRow;

	                if (invKsRow % 4) {
	                    var t = keySchedule[ksRow];
	                } else {
	                    var t = keySchedule[ksRow - 4];
	                }

	                if (invKsRow < 4 || ksRow <= 4) {
	                    invKeySchedule[invKsRow] = t;
	                } else {
	                    invKeySchedule[invKsRow] = INV_SUB_MIX_0[SBOX[t >>> 24]] ^ INV_SUB_MIX_1[SBOX[(t >>> 16) & 0xff]] ^
	                                               INV_SUB_MIX_2[SBOX[(t >>> 8) & 0xff]] ^ INV_SUB_MIX_3[SBOX[t & 0xff]];
	                }
	            }
	        },

	        encryptBlock: function (M, offset) {
	            this._doCryptBlock(M, offset, this._keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX);
	        },

	        decryptBlock: function (M, offset) {
	            // Swap 2nd and 4th rows
	            var t = M[offset + 1];
	            M[offset + 1] = M[offset + 3];
	            M[offset + 3] = t;

	            this._doCryptBlock(M, offset, this._invKeySchedule, INV_SUB_MIX_0, INV_SUB_MIX_1, INV_SUB_MIX_2, INV_SUB_MIX_3, INV_SBOX);

	            // Inv swap 2nd and 4th rows
	            var t = M[offset + 1];
	            M[offset + 1] = M[offset + 3];
	            M[offset + 3] = t;
	        },

	        _doCryptBlock: function (M, offset, keySchedule, SUB_MIX_0, SUB_MIX_1, SUB_MIX_2, SUB_MIX_3, SBOX) {
	            // Shortcut
	            var nRounds = this._nRounds;

	            // Get input, add round key
	            var s0 = M[offset]     ^ keySchedule[0];
	            var s1 = M[offset + 1] ^ keySchedule[1];
	            var s2 = M[offset + 2] ^ keySchedule[2];
	            var s3 = M[offset + 3] ^ keySchedule[3];

	            // Key schedule row counter
	            var ksRow = 4;

	            // Rounds
	            for (var round = 1; round < nRounds; round++) {
	                // Shift rows, sub bytes, mix columns, add round key
	                var t0 = SUB_MIX_0[s0 >>> 24] ^ SUB_MIX_1[(s1 >>> 16) & 0xff] ^ SUB_MIX_2[(s2 >>> 8) & 0xff] ^ SUB_MIX_3[s3 & 0xff] ^ keySchedule[ksRow++];
	                var t1 = SUB_MIX_0[s1 >>> 24] ^ SUB_MIX_1[(s2 >>> 16) & 0xff] ^ SUB_MIX_2[(s3 >>> 8) & 0xff] ^ SUB_MIX_3[s0 & 0xff] ^ keySchedule[ksRow++];
	                var t2 = SUB_MIX_0[s2 >>> 24] ^ SUB_MIX_1[(s3 >>> 16) & 0xff] ^ SUB_MIX_2[(s0 >>> 8) & 0xff] ^ SUB_MIX_3[s1 & 0xff] ^ keySchedule[ksRow++];
	                var t3 = SUB_MIX_0[s3 >>> 24] ^ SUB_MIX_1[(s0 >>> 16) & 0xff] ^ SUB_MIX_2[(s1 >>> 8) & 0xff] ^ SUB_MIX_3[s2 & 0xff] ^ keySchedule[ksRow++];

	                // Update state
	                s0 = t0;
	                s1 = t1;
	                s2 = t2;
	                s3 = t3;
	            }

	            // Shift rows, sub bytes, add round key
	            var t0 = ((SBOX[s0 >>> 24] << 24) | (SBOX[(s1 >>> 16) & 0xff] << 16) | (SBOX[(s2 >>> 8) & 0xff] << 8) | SBOX[s3 & 0xff]) ^ keySchedule[ksRow++];
	            var t1 = ((SBOX[s1 >>> 24] << 24) | (SBOX[(s2 >>> 16) & 0xff] << 16) | (SBOX[(s3 >>> 8) & 0xff] << 8) | SBOX[s0 & 0xff]) ^ keySchedule[ksRow++];
	            var t2 = ((SBOX[s2 >>> 24] << 24) | (SBOX[(s3 >>> 16) & 0xff] << 16) | (SBOX[(s0 >>> 8) & 0xff] << 8) | SBOX[s1 & 0xff]) ^ keySchedule[ksRow++];
	            var t3 = ((SBOX[s3 >>> 24] << 24) | (SBOX[(s0 >>> 16) & 0xff] << 16) | (SBOX[(s1 >>> 8) & 0xff] << 8) | SBOX[s2 & 0xff]) ^ keySchedule[ksRow++];

	            // Set output
	            M[offset]     = t0;
	            M[offset + 1] = t1;
	            M[offset + 2] = t2;
	            M[offset + 3] = t3;
	        },

	        keySize: 256/32
	    });

	    /**
	     * Shortcut functions to the cipher's object interface.
	     *
	     * @example
	     *
	     *     var ciphertext = CryptoJS.AES.encrypt(message, key, cfg);
	     *     var plaintext  = CryptoJS.AES.decrypt(ciphertext, key, cfg);
	     */
	    C.AES = BlockCipher._createHelper(AES);
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var WordArray = C_lib.WordArray;
	    var BlockCipher = C_lib.BlockCipher;
	    var C_algo = C.algo;

	    // Permuted Choice 1 constants
	    var PC1 = [
	        57, 49, 41, 33, 25, 17, 9,  1,
	        58, 50, 42, 34, 26, 18, 10, 2,
	        59, 51, 43, 35, 27, 19, 11, 3,
	        60, 52, 44, 36, 63, 55, 47, 39,
	        31, 23, 15, 7,  62, 54, 46, 38,
	        30, 22, 14, 6,  61, 53, 45, 37,
	        29, 21, 13, 5,  28, 20, 12, 4
	    ];

	    // Permuted Choice 2 constants
	    var PC2 = [
	        14, 17, 11, 24, 1,  5,
	        3,  28, 15, 6,  21, 10,
	        23, 19, 12, 4,  26, 8,
	        16, 7,  27, 20, 13, 2,
	        41, 52, 31, 37, 47, 55,
	        30, 40, 51, 45, 33, 48,
	        44, 49, 39, 56, 34, 53,
	        46, 42, 50, 36, 29, 32
	    ];

	    // Cumulative bit shift constants
	    var BIT_SHIFTS = [1,  2,  4,  6,  8,  10, 12, 14, 15, 17, 19, 21, 23, 25, 27, 28];

	    // SBOXes and round permutation constants
	    var SBOX_P = [
	        {
	            0x0: 0x808200,
	            0x10000000: 0x8000,
	            0x20000000: 0x808002,
	            0x30000000: 0x2,
	            0x40000000: 0x200,
	            0x50000000: 0x808202,
	            0x60000000: 0x800202,
	            0x70000000: 0x800000,
	            0x80000000: 0x202,
	            0x90000000: 0x800200,
	            0xa0000000: 0x8200,
	            0xb0000000: 0x808000,
	            0xc0000000: 0x8002,
	            0xd0000000: 0x800002,
	            0xe0000000: 0x0,
	            0xf0000000: 0x8202,
	            0x8000000: 0x0,
	            0x18000000: 0x808202,
	            0x28000000: 0x8202,
	            0x38000000: 0x8000,
	            0x48000000: 0x808200,
	            0x58000000: 0x200,
	            0x68000000: 0x808002,
	            0x78000000: 0x2,
	            0x88000000: 0x800200,
	            0x98000000: 0x8200,
	            0xa8000000: 0x808000,
	            0xb8000000: 0x800202,
	            0xc8000000: 0x800002,
	            0xd8000000: 0x8002,
	            0xe8000000: 0x202,
	            0xf8000000: 0x800000,
	            0x1: 0x8000,
	            0x10000001: 0x2,
	            0x20000001: 0x808200,
	            0x30000001: 0x800000,
	            0x40000001: 0x808002,
	            0x50000001: 0x8200,
	            0x60000001: 0x200,
	            0x70000001: 0x800202,
	            0x80000001: 0x808202,
	            0x90000001: 0x808000,
	            0xa0000001: 0x800002,
	            0xb0000001: 0x8202,
	            0xc0000001: 0x202,
	            0xd0000001: 0x800200,
	            0xe0000001: 0x8002,
	            0xf0000001: 0x0,
	            0x8000001: 0x808202,
	            0x18000001: 0x808000,
	            0x28000001: 0x800000,
	            0x38000001: 0x200,
	            0x48000001: 0x8000,
	            0x58000001: 0x800002,
	            0x68000001: 0x2,
	            0x78000001: 0x8202,
	            0x88000001: 0x8002,
	            0x98000001: 0x800202,
	            0xa8000001: 0x202,
	            0xb8000001: 0x808200,
	            0xc8000001: 0x800200,
	            0xd8000001: 0x0,
	            0xe8000001: 0x8200,
	            0xf8000001: 0x808002
	        },
	        {
	            0x0: 0x40084010,
	            0x1000000: 0x4000,
	            0x2000000: 0x80000,
	            0x3000000: 0x40080010,
	            0x4000000: 0x40000010,
	            0x5000000: 0x40084000,
	            0x6000000: 0x40004000,
	            0x7000000: 0x10,
	            0x8000000: 0x84000,
	            0x9000000: 0x40004010,
	            0xa000000: 0x40000000,
	            0xb000000: 0x84010,
	            0xc000000: 0x80010,
	            0xd000000: 0x0,
	            0xe000000: 0x4010,
	            0xf000000: 0x40080000,
	            0x800000: 0x40004000,
	            0x1800000: 0x84010,
	            0x2800000: 0x10,
	            0x3800000: 0x40004010,
	            0x4800000: 0x40084010,
	            0x5800000: 0x40000000,
	            0x6800000: 0x80000,
	            0x7800000: 0x40080010,
	            0x8800000: 0x80010,
	            0x9800000: 0x0,
	            0xa800000: 0x4000,
	            0xb800000: 0x40080000,
	            0xc800000: 0x40000010,
	            0xd800000: 0x84000,
	            0xe800000: 0x40084000,
	            0xf800000: 0x4010,
	            0x10000000: 0x0,
	            0x11000000: 0x40080010,
	            0x12000000: 0x40004010,
	            0x13000000: 0x40084000,
	            0x14000000: 0x40080000,
	            0x15000000: 0x10,
	            0x16000000: 0x84010,
	            0x17000000: 0x4000,
	            0x18000000: 0x4010,
	            0x19000000: 0x80000,
	            0x1a000000: 0x80010,
	            0x1b000000: 0x40000010,
	            0x1c000000: 0x84000,
	            0x1d000000: 0x40004000,
	            0x1e000000: 0x40000000,
	            0x1f000000: 0x40084010,
	            0x10800000: 0x84010,
	            0x11800000: 0x80000,
	            0x12800000: 0x40080000,
	            0x13800000: 0x4000,
	            0x14800000: 0x40004000,
	            0x15800000: 0x40084010,
	            0x16800000: 0x10,
	            0x17800000: 0x40000000,
	            0x18800000: 0x40084000,
	            0x19800000: 0x40000010,
	            0x1a800000: 0x40004010,
	            0x1b800000: 0x80010,
	            0x1c800000: 0x0,
	            0x1d800000: 0x4010,
	            0x1e800000: 0x40080010,
	            0x1f800000: 0x84000
	        },
	        {
	            0x0: 0x104,
	            0x100000: 0x0,
	            0x200000: 0x4000100,
	            0x300000: 0x10104,
	            0x400000: 0x10004,
	            0x500000: 0x4000004,
	            0x600000: 0x4010104,
	            0x700000: 0x4010000,
	            0x800000: 0x4000000,
	            0x900000: 0x4010100,
	            0xa00000: 0x10100,
	            0xb00000: 0x4010004,
	            0xc00000: 0x4000104,
	            0xd00000: 0x10000,
	            0xe00000: 0x4,
	            0xf00000: 0x100,
	            0x80000: 0x4010100,
	            0x180000: 0x4010004,
	            0x280000: 0x0,
	            0x380000: 0x4000100,
	            0x480000: 0x4000004,
	            0x580000: 0x10000,
	            0x680000: 0x10004,
	            0x780000: 0x104,
	            0x880000: 0x4,
	            0x980000: 0x100,
	            0xa80000: 0x4010000,
	            0xb80000: 0x10104,
	            0xc80000: 0x10100,
	            0xd80000: 0x4000104,
	            0xe80000: 0x4010104,
	            0xf80000: 0x4000000,
	            0x1000000: 0x4010100,
	            0x1100000: 0x10004,
	            0x1200000: 0x10000,
	            0x1300000: 0x4000100,
	            0x1400000: 0x100,
	            0x1500000: 0x4010104,
	            0x1600000: 0x4000004,
	            0x1700000: 0x0,
	            0x1800000: 0x4000104,
	            0x1900000: 0x4000000,
	            0x1a00000: 0x4,
	            0x1b00000: 0x10100,
	            0x1c00000: 0x4010000,
	            0x1d00000: 0x104,
	            0x1e00000: 0x10104,
	            0x1f00000: 0x4010004,
	            0x1080000: 0x4000000,
	            0x1180000: 0x104,
	            0x1280000: 0x4010100,
	            0x1380000: 0x0,
	            0x1480000: 0x10004,
	            0x1580000: 0x4000100,
	            0x1680000: 0x100,
	            0x1780000: 0x4010004,
	            0x1880000: 0x10000,
	            0x1980000: 0x4010104,
	            0x1a80000: 0x10104,
	            0x1b80000: 0x4000004,
	            0x1c80000: 0x4000104,
	            0x1d80000: 0x4010000,
	            0x1e80000: 0x4,
	            0x1f80000: 0x10100
	        },
	        {
	            0x0: 0x80401000,
	            0x10000: 0x80001040,
	            0x20000: 0x401040,
	            0x30000: 0x80400000,
	            0x40000: 0x0,
	            0x50000: 0x401000,
	            0x60000: 0x80000040,
	            0x70000: 0x400040,
	            0x80000: 0x80000000,
	            0x90000: 0x400000,
	            0xa0000: 0x40,
	            0xb0000: 0x80001000,
	            0xc0000: 0x80400040,
	            0xd0000: 0x1040,
	            0xe0000: 0x1000,
	            0xf0000: 0x80401040,
	            0x8000: 0x80001040,
	            0x18000: 0x40,
	            0x28000: 0x80400040,
	            0x38000: 0x80001000,
	            0x48000: 0x401000,
	            0x58000: 0x80401040,
	            0x68000: 0x0,
	            0x78000: 0x80400000,
	            0x88000: 0x1000,
	            0x98000: 0x80401000,
	            0xa8000: 0x400000,
	            0xb8000: 0x1040,
	            0xc8000: 0x80000000,
	            0xd8000: 0x400040,
	            0xe8000: 0x401040,
	            0xf8000: 0x80000040,
	            0x100000: 0x400040,
	            0x110000: 0x401000,
	            0x120000: 0x80000040,
	            0x130000: 0x0,
	            0x140000: 0x1040,
	            0x150000: 0x80400040,
	            0x160000: 0x80401000,
	            0x170000: 0x80001040,
	            0x180000: 0x80401040,
	            0x190000: 0x80000000,
	            0x1a0000: 0x80400000,
	            0x1b0000: 0x401040,
	            0x1c0000: 0x80001000,
	            0x1d0000: 0x400000,
	            0x1e0000: 0x40,
	            0x1f0000: 0x1000,
	            0x108000: 0x80400000,
	            0x118000: 0x80401040,
	            0x128000: 0x0,
	            0x138000: 0x401000,
	            0x148000: 0x400040,
	            0x158000: 0x80000000,
	            0x168000: 0x80001040,
	            0x178000: 0x40,
	            0x188000: 0x80000040,
	            0x198000: 0x1000,
	            0x1a8000: 0x80001000,
	            0x1b8000: 0x80400040,
	            0x1c8000: 0x1040,
	            0x1d8000: 0x80401000,
	            0x1e8000: 0x400000,
	            0x1f8000: 0x401040
	        },
	        {
	            0x0: 0x80,
	            0x1000: 0x1040000,
	            0x2000: 0x40000,
	            0x3000: 0x20000000,
	            0x4000: 0x20040080,
	            0x5000: 0x1000080,
	            0x6000: 0x21000080,
	            0x7000: 0x40080,
	            0x8000: 0x1000000,
	            0x9000: 0x20040000,
	            0xa000: 0x20000080,
	            0xb000: 0x21040080,
	            0xc000: 0x21040000,
	            0xd000: 0x0,
	            0xe000: 0x1040080,
	            0xf000: 0x21000000,
	            0x800: 0x1040080,
	            0x1800: 0x21000080,
	            0x2800: 0x80,
	            0x3800: 0x1040000,
	            0x4800: 0x40000,
	            0x5800: 0x20040080,
	            0x6800: 0x21040000,
	            0x7800: 0x20000000,
	            0x8800: 0x20040000,
	            0x9800: 0x0,
	            0xa800: 0x21040080,
	            0xb800: 0x1000080,
	            0xc800: 0x20000080,
	            0xd800: 0x21000000,
	            0xe800: 0x1000000,
	            0xf800: 0x40080,
	            0x10000: 0x40000,
	            0x11000: 0x80,
	            0x12000: 0x20000000,
	            0x13000: 0x21000080,
	            0x14000: 0x1000080,
	            0x15000: 0x21040000,
	            0x16000: 0x20040080,
	            0x17000: 0x1000000,
	            0x18000: 0x21040080,
	            0x19000: 0x21000000,
	            0x1a000: 0x1040000,
	            0x1b000: 0x20040000,
	            0x1c000: 0x40080,
	            0x1d000: 0x20000080,
	            0x1e000: 0x0,
	            0x1f000: 0x1040080,
	            0x10800: 0x21000080,
	            0x11800: 0x1000000,
	            0x12800: 0x1040000,
	            0x13800: 0x20040080,
	            0x14800: 0x20000000,
	            0x15800: 0x1040080,
	            0x16800: 0x80,
	            0x17800: 0x21040000,
	            0x18800: 0x40080,
	            0x19800: 0x21040080,
	            0x1a800: 0x0,
	            0x1b800: 0x21000000,
	            0x1c800: 0x1000080,
	            0x1d800: 0x40000,
	            0x1e800: 0x20040000,
	            0x1f800: 0x20000080
	        },
	        {
	            0x0: 0x10000008,
	            0x100: 0x2000,
	            0x200: 0x10200000,
	            0x300: 0x10202008,
	            0x400: 0x10002000,
	            0x500: 0x200000,
	            0x600: 0x200008,
	            0x700: 0x10000000,
	            0x800: 0x0,
	            0x900: 0x10002008,
	            0xa00: 0x202000,
	            0xb00: 0x8,
	            0xc00: 0x10200008,
	            0xd00: 0x202008,
	            0xe00: 0x2008,
	            0xf00: 0x10202000,
	            0x80: 0x10200000,
	            0x180: 0x10202008,
	            0x280: 0x8,
	            0x380: 0x200000,
	            0x480: 0x202008,
	            0x580: 0x10000008,
	            0x680: 0x10002000,
	            0x780: 0x2008,
	            0x880: 0x200008,
	            0x980: 0x2000,
	            0xa80: 0x10002008,
	            0xb80: 0x10200008,
	            0xc80: 0x0,
	            0xd80: 0x10202000,
	            0xe80: 0x202000,
	            0xf80: 0x10000000,
	            0x1000: 0x10002000,
	            0x1100: 0x10200008,
	            0x1200: 0x10202008,
	            0x1300: 0x2008,
	            0x1400: 0x200000,
	            0x1500: 0x10000000,
	            0x1600: 0x10000008,
	            0x1700: 0x202000,
	            0x1800: 0x202008,
	            0x1900: 0x0,
	            0x1a00: 0x8,
	            0x1b00: 0x10200000,
	            0x1c00: 0x2000,
	            0x1d00: 0x10002008,
	            0x1e00: 0x10202000,
	            0x1f00: 0x200008,
	            0x1080: 0x8,
	            0x1180: 0x202000,
	            0x1280: 0x200000,
	            0x1380: 0x10000008,
	            0x1480: 0x10002000,
	            0x1580: 0x2008,
	            0x1680: 0x10202008,
	            0x1780: 0x10200000,
	            0x1880: 0x10202000,
	            0x1980: 0x10200008,
	            0x1a80: 0x2000,
	            0x1b80: 0x202008,
	            0x1c80: 0x200008,
	            0x1d80: 0x0,
	            0x1e80: 0x10000000,
	            0x1f80: 0x10002008
	        },
	        {
	            0x0: 0x100000,
	            0x10: 0x2000401,
	            0x20: 0x400,
	            0x30: 0x100401,
	            0x40: 0x2100401,
	            0x50: 0x0,
	            0x60: 0x1,
	            0x70: 0x2100001,
	            0x80: 0x2000400,
	            0x90: 0x100001,
	            0xa0: 0x2000001,
	            0xb0: 0x2100400,
	            0xc0: 0x2100000,
	            0xd0: 0x401,
	            0xe0: 0x100400,
	            0xf0: 0x2000000,
	            0x8: 0x2100001,
	            0x18: 0x0,
	            0x28: 0x2000401,
	            0x38: 0x2100400,
	            0x48: 0x100000,
	            0x58: 0x2000001,
	            0x68: 0x2000000,
	            0x78: 0x401,
	            0x88: 0x100401,
	            0x98: 0x2000400,
	            0xa8: 0x2100000,
	            0xb8: 0x100001,
	            0xc8: 0x400,
	            0xd8: 0x2100401,
	            0xe8: 0x1,
	            0xf8: 0x100400,
	            0x100: 0x2000000,
	            0x110: 0x100000,
	            0x120: 0x2000401,
	            0x130: 0x2100001,
	            0x140: 0x100001,
	            0x150: 0x2000400,
	            0x160: 0x2100400,
	            0x170: 0x100401,
	            0x180: 0x401,
	            0x190: 0x2100401,
	            0x1a0: 0x100400,
	            0x1b0: 0x1,
	            0x1c0: 0x0,
	            0x1d0: 0x2100000,
	            0x1e0: 0x2000001,
	            0x1f0: 0x400,
	            0x108: 0x100400,
	            0x118: 0x2000401,
	            0x128: 0x2100001,
	            0x138: 0x1,
	            0x148: 0x2000000,
	            0x158: 0x100000,
	            0x168: 0x401,
	            0x178: 0x2100400,
	            0x188: 0x2000001,
	            0x198: 0x2100000,
	            0x1a8: 0x0,
	            0x1b8: 0x2100401,
	            0x1c8: 0x100401,
	            0x1d8: 0x400,
	            0x1e8: 0x2000400,
	            0x1f8: 0x100001
	        },
	        {
	            0x0: 0x8000820,
	            0x1: 0x20000,
	            0x2: 0x8000000,
	            0x3: 0x20,
	            0x4: 0x20020,
	            0x5: 0x8020820,
	            0x6: 0x8020800,
	            0x7: 0x800,
	            0x8: 0x8020000,
	            0x9: 0x8000800,
	            0xa: 0x20800,
	            0xb: 0x8020020,
	            0xc: 0x820,
	            0xd: 0x0,
	            0xe: 0x8000020,
	            0xf: 0x20820,
	            0x80000000: 0x800,
	            0x80000001: 0x8020820,
	            0x80000002: 0x8000820,
	            0x80000003: 0x8000000,
	            0x80000004: 0x8020000,
	            0x80000005: 0x20800,
	            0x80000006: 0x20820,
	            0x80000007: 0x20,
	            0x80000008: 0x8000020,
	            0x80000009: 0x820,
	            0x8000000a: 0x20020,
	            0x8000000b: 0x8020800,
	            0x8000000c: 0x0,
	            0x8000000d: 0x8020020,
	            0x8000000e: 0x8000800,
	            0x8000000f: 0x20000,
	            0x10: 0x20820,
	            0x11: 0x8020800,
	            0x12: 0x20,
	            0x13: 0x800,
	            0x14: 0x8000800,
	            0x15: 0x8000020,
	            0x16: 0x8020020,
	            0x17: 0x20000,
	            0x18: 0x0,
	            0x19: 0x20020,
	            0x1a: 0x8020000,
	            0x1b: 0x8000820,
	            0x1c: 0x8020820,
	            0x1d: 0x20800,
	            0x1e: 0x820,
	            0x1f: 0x8000000,
	            0x80000010: 0x20000,
	            0x80000011: 0x800,
	            0x80000012: 0x8020020,
	            0x80000013: 0x20820,
	            0x80000014: 0x20,
	            0x80000015: 0x8020000,
	            0x80000016: 0x8000000,
	            0x80000017: 0x8000820,
	            0x80000018: 0x8020820,
	            0x80000019: 0x8000020,
	            0x8000001a: 0x8000800,
	            0x8000001b: 0x0,
	            0x8000001c: 0x20800,
	            0x8000001d: 0x820,
	            0x8000001e: 0x20020,
	            0x8000001f: 0x8020800
	        }
	    ];

	    // Masks that select the SBOX input
	    var SBOX_MASK = [
	        0xf8000001, 0x1f800000, 0x01f80000, 0x001f8000,
	        0x0001f800, 0x00001f80, 0x000001f8, 0x8000001f
	    ];

	    /**
	     * DES block cipher algorithm.
	     */
	    var DES = C_algo.DES = BlockCipher.extend({
	        _doReset: function () {
	            // Shortcuts
	            var key = this._key;
	            var keyWords = key.words;

	            // Select 56 bits according to PC1
	            var keyBits = [];
	            for (var i = 0; i < 56; i++) {
	                var keyBitPos = PC1[i] - 1;
	                keyBits[i] = (keyWords[keyBitPos >>> 5] >>> (31 - keyBitPos % 32)) & 1;
	            }

	            // Assemble 16 subkeys
	            var subKeys = this._subKeys = [];
	            for (var nSubKey = 0; nSubKey < 16; nSubKey++) {
	                // Create subkey
	                var subKey = subKeys[nSubKey] = [];

	                // Shortcut
	                var bitShift = BIT_SHIFTS[nSubKey];

	                // Select 48 bits according to PC2
	                for (var i = 0; i < 24; i++) {
	                    // Select from the left 28 key bits
	                    subKey[(i / 6) | 0] |= keyBits[((PC2[i] - 1) + bitShift) % 28] << (31 - i % 6);

	                    // Select from the right 28 key bits
	                    subKey[4 + ((i / 6) | 0)] |= keyBits[28 + (((PC2[i + 24] - 1) + bitShift) % 28)] << (31 - i % 6);
	                }

	                // Since each subkey is applied to an expanded 32-bit input,
	                // the subkey can be broken into 8 values scaled to 32-bits,
	                // which allows the key to be used without expansion
	                subKey[0] = (subKey[0] << 1) | (subKey[0] >>> 31);
	                for (var i = 1; i < 7; i++) {
	                    subKey[i] = subKey[i] >>> ((i - 1) * 4 + 3);
	                }
	                subKey[7] = (subKey[7] << 5) | (subKey[7] >>> 27);
	            }

	            // Compute inverse subkeys
	            var invSubKeys = this._invSubKeys = [];
	            for (var i = 0; i < 16; i++) {
	                invSubKeys[i] = subKeys[15 - i];
	            }
	        },

	        encryptBlock: function (M, offset) {
	            this._doCryptBlock(M, offset, this._subKeys);
	        },

	        decryptBlock: function (M, offset) {
	            this._doCryptBlock(M, offset, this._invSubKeys);
	        },

	        _doCryptBlock: function (M, offset, subKeys) {
	            // Get input
	            this._lBlock = M[offset];
	            this._rBlock = M[offset + 1];

	            // Initial permutation
	            exchangeLR.call(this, 4,  0x0f0f0f0f);
	            exchangeLR.call(this, 16, 0x0000ffff);
	            exchangeRL.call(this, 2,  0x33333333);
	            exchangeRL.call(this, 8,  0x00ff00ff);
	            exchangeLR.call(this, 1,  0x55555555);

	            // Rounds
	            for (var round = 0; round < 16; round++) {
	                // Shortcuts
	                var subKey = subKeys[round];
	                var lBlock = this._lBlock;
	                var rBlock = this._rBlock;

	                // Feistel function
	                var f = 0;
	                for (var i = 0; i < 8; i++) {
	                    f |= SBOX_P[i][((rBlock ^ subKey[i]) & SBOX_MASK[i]) >>> 0];
	                }
	                this._lBlock = rBlock;
	                this._rBlock = lBlock ^ f;
	            }

	            // Undo swap from last round
	            var t = this._lBlock;
	            this._lBlock = this._rBlock;
	            this._rBlock = t;

	            // Final permutation
	            exchangeLR.call(this, 1,  0x55555555);
	            exchangeRL.call(this, 8,  0x00ff00ff);
	            exchangeRL.call(this, 2,  0x33333333);
	            exchangeLR.call(this, 16, 0x0000ffff);
	            exchangeLR.call(this, 4,  0x0f0f0f0f);

	            // Set output
	            M[offset] = this._lBlock;
	            M[offset + 1] = this._rBlock;
	        },

	        keySize: 64/32,

	        ivSize: 64/32,

	        blockSize: 64/32
	    });

	    // Swap bits across the left and right words
	    function exchangeLR(offset, mask) {
	        var t = ((this._lBlock >>> offset) ^ this._rBlock) & mask;
	        this._rBlock ^= t;
	        this._lBlock ^= t << offset;
	    }

	    function exchangeRL(offset, mask) {
	        var t = ((this._rBlock >>> offset) ^ this._lBlock) & mask;
	        this._lBlock ^= t;
	        this._rBlock ^= t << offset;
	    }

	    /**
	     * Shortcut functions to the cipher's object interface.
	     *
	     * @example
	     *
	     *     var ciphertext = CryptoJS.DES.encrypt(message, key, cfg);
	     *     var plaintext  = CryptoJS.DES.decrypt(ciphertext, key, cfg);
	     */
	    C.DES = BlockCipher._createHelper(DES);

	    /**
	     * Triple-DES block cipher algorithm.
	     */
	    var TripleDES = C_algo.TripleDES = BlockCipher.extend({
	        _doReset: function () {
	            // Shortcuts
	            var key = this._key;
	            var keyWords = key.words;
	            // Make sure the key length is valid (64, 128 or >= 192 bit)
	            if (keyWords.length !== 2 && keyWords.length !== 4 && keyWords.length < 6) {
	                throw new Error('Invalid key length - 3DES requires the key length to be 64, 128, 192 or >192.');
	            }

	            // Extend the key according to the keying options defined in 3DES standard
	            var key1 = keyWords.slice(0, 2);
	            var key2 = keyWords.length < 4 ? keyWords.slice(0, 2) : keyWords.slice(2, 4);
	            var key3 = keyWords.length < 6 ? keyWords.slice(0, 2) : keyWords.slice(4, 6);

	            // Create DES instances
	            this._des1 = DES.createEncryptor(WordArray.create(key1));
	            this._des2 = DES.createEncryptor(WordArray.create(key2));
	            this._des3 = DES.createEncryptor(WordArray.create(key3));
	        },

	        encryptBlock: function (M, offset) {
	            this._des1.encryptBlock(M, offset);
	            this._des2.decryptBlock(M, offset);
	            this._des3.encryptBlock(M, offset);
	        },

	        decryptBlock: function (M, offset) {
	            this._des3.decryptBlock(M, offset);
	            this._des2.encryptBlock(M, offset);
	            this._des1.decryptBlock(M, offset);
	        },

	        keySize: 192/32,

	        ivSize: 64/32,

	        blockSize: 64/32
	    });

	    /**
	     * Shortcut functions to the cipher's object interface.
	     *
	     * @example
	     *
	     *     var ciphertext = CryptoJS.TripleDES.encrypt(message, key, cfg);
	     *     var plaintext  = CryptoJS.TripleDES.decrypt(ciphertext, key, cfg);
	     */
	    C.TripleDES = BlockCipher._createHelper(TripleDES);
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var StreamCipher = C_lib.StreamCipher;
	    var C_algo = C.algo;

	    /**
	     * RC4 stream cipher algorithm.
	     */
	    var RC4 = C_algo.RC4 = StreamCipher.extend({
	        _doReset: function () {
	            // Shortcuts
	            var key = this._key;
	            var keyWords = key.words;
	            var keySigBytes = key.sigBytes;

	            // Init sbox
	            var S = this._S = [];
	            for (var i = 0; i < 256; i++) {
	                S[i] = i;
	            }

	            // Key setup
	            for (var i = 0, j = 0; i < 256; i++) {
	                var keyByteIndex = i % keySigBytes;
	                var keyByte = (keyWords[keyByteIndex >>> 2] >>> (24 - (keyByteIndex % 4) * 8)) & 0xff;

	                j = (j + S[i] + keyByte) % 256;

	                // Swap
	                var t = S[i];
	                S[i] = S[j];
	                S[j] = t;
	            }

	            // Counters
	            this._i = this._j = 0;
	        },

	        _doProcessBlock: function (M, offset) {
	            M[offset] ^= generateKeystreamWord.call(this);
	        },

	        keySize: 256/32,

	        ivSize: 0
	    });

	    function generateKeystreamWord() {
	        // Shortcuts
	        var S = this._S;
	        var i = this._i;
	        var j = this._j;

	        // Generate keystream word
	        var keystreamWord = 0;
	        for (var n = 0; n < 4; n++) {
	            i = (i + 1) % 256;
	            j = (j + S[i]) % 256;

	            // Swap
	            var t = S[i];
	            S[i] = S[j];
	            S[j] = t;

	            keystreamWord |= S[(S[i] + S[j]) % 256] << (24 - n * 8);
	        }

	        // Update counters
	        this._i = i;
	        this._j = j;

	        return keystreamWord;
	    }

	    /**
	     * Shortcut functions to the cipher's object interface.
	     *
	     * @example
	     *
	     *     var ciphertext = CryptoJS.RC4.encrypt(message, key, cfg);
	     *     var plaintext  = CryptoJS.RC4.decrypt(ciphertext, key, cfg);
	     */
	    C.RC4 = StreamCipher._createHelper(RC4);

	    /**
	     * Modified RC4 stream cipher algorithm.
	     */
	    var RC4Drop = C_algo.RC4Drop = RC4.extend({
	        /**
	         * Configuration options.
	         *
	         * @property {number} drop The number of keystream words to drop. Default 192
	         */
	        cfg: RC4.cfg.extend({
	            drop: 192
	        }),

	        _doReset: function () {
	            RC4._doReset.call(this);

	            // Drop
	            for (var i = this.cfg.drop; i > 0; i--) {
	                generateKeystreamWord.call(this);
	            }
	        }
	    });

	    /**
	     * Shortcut functions to the cipher's object interface.
	     *
	     * @example
	     *
	     *     var ciphertext = CryptoJS.RC4Drop.encrypt(message, key, cfg);
	     *     var plaintext  = CryptoJS.RC4Drop.decrypt(ciphertext, key, cfg);
	     */
	    C.RC4Drop = StreamCipher._createHelper(RC4Drop);
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var StreamCipher = C_lib.StreamCipher;
	    var C_algo = C.algo;

	    // Reusable objects
	    var S  = [];
	    var C_ = [];
	    var G  = [];

	    /**
	     * Rabbit stream cipher algorithm
	     */
	    var Rabbit = C_algo.Rabbit = StreamCipher.extend({
	        _doReset: function () {
	            // Shortcuts
	            var K = this._key.words;
	            var iv = this.cfg.iv;

	            // Swap endian
	            for (var i = 0; i < 4; i++) {
	                K[i] = (((K[i] << 8)  | (K[i] >>> 24)) & 0x00ff00ff) |
	                       (((K[i] << 24) | (K[i] >>> 8))  & 0xff00ff00);
	            }

	            // Generate initial state values
	            var X = this._X = [
	                K[0], (K[3] << 16) | (K[2] >>> 16),
	                K[1], (K[0] << 16) | (K[3] >>> 16),
	                K[2], (K[1] << 16) | (K[0] >>> 16),
	                K[3], (K[2] << 16) | (K[1] >>> 16)
	            ];

	            // Generate initial counter values
	            var C = this._C = [
	                (K[2] << 16) | (K[2] >>> 16), (K[0] & 0xffff0000) | (K[1] & 0x0000ffff),
	                (K[3] << 16) | (K[3] >>> 16), (K[1] & 0xffff0000) | (K[2] & 0x0000ffff),
	                (K[0] << 16) | (K[0] >>> 16), (K[2] & 0xffff0000) | (K[3] & 0x0000ffff),
	                (K[1] << 16) | (K[1] >>> 16), (K[3] & 0xffff0000) | (K[0] & 0x0000ffff)
	            ];

	            // Carry bit
	            this._b = 0;

	            // Iterate the system four times
	            for (var i = 0; i < 4; i++) {
	                nextState.call(this);
	            }

	            // Modify the counters
	            for (var i = 0; i < 8; i++) {
	                C[i] ^= X[(i + 4) & 7];
	            }

	            // IV setup
	            if (iv) {
	                // Shortcuts
	                var IV = iv.words;
	                var IV_0 = IV[0];
	                var IV_1 = IV[1];

	                // Generate four subvectors
	                var i0 = (((IV_0 << 8) | (IV_0 >>> 24)) & 0x00ff00ff) | (((IV_0 << 24) | (IV_0 >>> 8)) & 0xff00ff00);
	                var i2 = (((IV_1 << 8) | (IV_1 >>> 24)) & 0x00ff00ff) | (((IV_1 << 24) | (IV_1 >>> 8)) & 0xff00ff00);
	                var i1 = (i0 >>> 16) | (i2 & 0xffff0000);
	                var i3 = (i2 << 16)  | (i0 & 0x0000ffff);

	                // Modify counter values
	                C[0] ^= i0;
	                C[1] ^= i1;
	                C[2] ^= i2;
	                C[3] ^= i3;
	                C[4] ^= i0;
	                C[5] ^= i1;
	                C[6] ^= i2;
	                C[7] ^= i3;

	                // Iterate the system four times
	                for (var i = 0; i < 4; i++) {
	                    nextState.call(this);
	                }
	            }
	        },

	        _doProcessBlock: function (M, offset) {
	            // Shortcut
	            var X = this._X;

	            // Iterate the system
	            nextState.call(this);

	            // Generate four keystream words
	            S[0] = X[0] ^ (X[5] >>> 16) ^ (X[3] << 16);
	            S[1] = X[2] ^ (X[7] >>> 16) ^ (X[5] << 16);
	            S[2] = X[4] ^ (X[1] >>> 16) ^ (X[7] << 16);
	            S[3] = X[6] ^ (X[3] >>> 16) ^ (X[1] << 16);

	            for (var i = 0; i < 4; i++) {
	                // Swap endian
	                S[i] = (((S[i] << 8)  | (S[i] >>> 24)) & 0x00ff00ff) |
	                       (((S[i] << 24) | (S[i] >>> 8))  & 0xff00ff00);

	                // Encrypt
	                M[offset + i] ^= S[i];
	            }
	        },

	        blockSize: 128/32,

	        ivSize: 64/32
	    });

	    function nextState() {
	        // Shortcuts
	        var X = this._X;
	        var C = this._C;

	        // Save old counter values
	        for (var i = 0; i < 8; i++) {
	            C_[i] = C[i];
	        }

	        // Calculate new counter values
	        C[0] = (C[0] + 0x4d34d34d + this._b) | 0;
	        C[1] = (C[1] + 0xd34d34d3 + ((C[0] >>> 0) < (C_[0] >>> 0) ? 1 : 0)) | 0;
	        C[2] = (C[2] + 0x34d34d34 + ((C[1] >>> 0) < (C_[1] >>> 0) ? 1 : 0)) | 0;
	        C[3] = (C[3] + 0x4d34d34d + ((C[2] >>> 0) < (C_[2] >>> 0) ? 1 : 0)) | 0;
	        C[4] = (C[4] + 0xd34d34d3 + ((C[3] >>> 0) < (C_[3] >>> 0) ? 1 : 0)) | 0;
	        C[5] = (C[5] + 0x34d34d34 + ((C[4] >>> 0) < (C_[4] >>> 0) ? 1 : 0)) | 0;
	        C[6] = (C[6] + 0x4d34d34d + ((C[5] >>> 0) < (C_[5] >>> 0) ? 1 : 0)) | 0;
	        C[7] = (C[7] + 0xd34d34d3 + ((C[6] >>> 0) < (C_[6] >>> 0) ? 1 : 0)) | 0;
	        this._b = (C[7] >>> 0) < (C_[7] >>> 0) ? 1 : 0;

	        // Calculate the g-values
	        for (var i = 0; i < 8; i++) {
	            var gx = X[i] + C[i];

	            // Construct high and low argument for squaring
	            var ga = gx & 0xffff;
	            var gb = gx >>> 16;

	            // Calculate high and low result of squaring
	            var gh = ((((ga * ga) >>> 17) + ga * gb) >>> 15) + gb * gb;
	            var gl = (((gx & 0xffff0000) * gx) | 0) + (((gx & 0x0000ffff) * gx) | 0);

	            // High XOR low
	            G[i] = gh ^ gl;
	        }

	        // Calculate new state values
	        X[0] = (G[0] + ((G[7] << 16) | (G[7] >>> 16)) + ((G[6] << 16) | (G[6] >>> 16))) | 0;
	        X[1] = (G[1] + ((G[0] << 8)  | (G[0] >>> 24)) + G[7]) | 0;
	        X[2] = (G[2] + ((G[1] << 16) | (G[1] >>> 16)) + ((G[0] << 16) | (G[0] >>> 16))) | 0;
	        X[3] = (G[3] + ((G[2] << 8)  | (G[2] >>> 24)) + G[1]) | 0;
	        X[4] = (G[4] + ((G[3] << 16) | (G[3] >>> 16)) + ((G[2] << 16) | (G[2] >>> 16))) | 0;
	        X[5] = (G[5] + ((G[4] << 8)  | (G[4] >>> 24)) + G[3]) | 0;
	        X[6] = (G[6] + ((G[5] << 16) | (G[5] >>> 16)) + ((G[4] << 16) | (G[4] >>> 16))) | 0;
	        X[7] = (G[7] + ((G[6] << 8)  | (G[6] >>> 24)) + G[5]) | 0;
	    }

	    /**
	     * Shortcut functions to the cipher's object interface.
	     *
	     * @example
	     *
	     *     var ciphertext = CryptoJS.Rabbit.encrypt(message, key, cfg);
	     *     var plaintext  = CryptoJS.Rabbit.decrypt(ciphertext, key, cfg);
	     */
	    C.Rabbit = StreamCipher._createHelper(Rabbit);
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var StreamCipher = C_lib.StreamCipher;
	    var C_algo = C.algo;

	    // Reusable objects
	    var S  = [];
	    var C_ = [];
	    var G  = [];

	    /**
	     * Rabbit stream cipher algorithm.
	     *
	     * This is a legacy version that neglected to convert the key to little-endian.
	     * This error doesn't affect the cipher's security,
	     * but it does affect its compatibility with other implementations.
	     */
	    var RabbitLegacy = C_algo.RabbitLegacy = StreamCipher.extend({
	        _doReset: function () {
	            // Shortcuts
	            var K = this._key.words;
	            var iv = this.cfg.iv;

	            // Generate initial state values
	            var X = this._X = [
	                K[0], (K[3] << 16) | (K[2] >>> 16),
	                K[1], (K[0] << 16) | (K[3] >>> 16),
	                K[2], (K[1] << 16) | (K[0] >>> 16),
	                K[3], (K[2] << 16) | (K[1] >>> 16)
	            ];

	            // Generate initial counter values
	            var C = this._C = [
	                (K[2] << 16) | (K[2] >>> 16), (K[0] & 0xffff0000) | (K[1] & 0x0000ffff),
	                (K[3] << 16) | (K[3] >>> 16), (K[1] & 0xffff0000) | (K[2] & 0x0000ffff),
	                (K[0] << 16) | (K[0] >>> 16), (K[2] & 0xffff0000) | (K[3] & 0x0000ffff),
	                (K[1] << 16) | (K[1] >>> 16), (K[3] & 0xffff0000) | (K[0] & 0x0000ffff)
	            ];

	            // Carry bit
	            this._b = 0;

	            // Iterate the system four times
	            for (var i = 0; i < 4; i++) {
	                nextState.call(this);
	            }

	            // Modify the counters
	            for (var i = 0; i < 8; i++) {
	                C[i] ^= X[(i + 4) & 7];
	            }

	            // IV setup
	            if (iv) {
	                // Shortcuts
	                var IV = iv.words;
	                var IV_0 = IV[0];
	                var IV_1 = IV[1];

	                // Generate four subvectors
	                var i0 = (((IV_0 << 8) | (IV_0 >>> 24)) & 0x00ff00ff) | (((IV_0 << 24) | (IV_0 >>> 8)) & 0xff00ff00);
	                var i2 = (((IV_1 << 8) | (IV_1 >>> 24)) & 0x00ff00ff) | (((IV_1 << 24) | (IV_1 >>> 8)) & 0xff00ff00);
	                var i1 = (i0 >>> 16) | (i2 & 0xffff0000);
	                var i3 = (i2 << 16)  | (i0 & 0x0000ffff);

	                // Modify counter values
	                C[0] ^= i0;
	                C[1] ^= i1;
	                C[2] ^= i2;
	                C[3] ^= i3;
	                C[4] ^= i0;
	                C[5] ^= i1;
	                C[6] ^= i2;
	                C[7] ^= i3;

	                // Iterate the system four times
	                for (var i = 0; i < 4; i++) {
	                    nextState.call(this);
	                }
	            }
	        },

	        _doProcessBlock: function (M, offset) {
	            // Shortcut
	            var X = this._X;

	            // Iterate the system
	            nextState.call(this);

	            // Generate four keystream words
	            S[0] = X[0] ^ (X[5] >>> 16) ^ (X[3] << 16);
	            S[1] = X[2] ^ (X[7] >>> 16) ^ (X[5] << 16);
	            S[2] = X[4] ^ (X[1] >>> 16) ^ (X[7] << 16);
	            S[3] = X[6] ^ (X[3] >>> 16) ^ (X[1] << 16);

	            for (var i = 0; i < 4; i++) {
	                // Swap endian
	                S[i] = (((S[i] << 8)  | (S[i] >>> 24)) & 0x00ff00ff) |
	                       (((S[i] << 24) | (S[i] >>> 8))  & 0xff00ff00);

	                // Encrypt
	                M[offset + i] ^= S[i];
	            }
	        },

	        blockSize: 128/32,

	        ivSize: 64/32
	    });

	    function nextState() {
	        // Shortcuts
	        var X = this._X;
	        var C = this._C;

	        // Save old counter values
	        for (var i = 0; i < 8; i++) {
	            C_[i] = C[i];
	        }

	        // Calculate new counter values
	        C[0] = (C[0] + 0x4d34d34d + this._b) | 0;
	        C[1] = (C[1] + 0xd34d34d3 + ((C[0] >>> 0) < (C_[0] >>> 0) ? 1 : 0)) | 0;
	        C[2] = (C[2] + 0x34d34d34 + ((C[1] >>> 0) < (C_[1] >>> 0) ? 1 : 0)) | 0;
	        C[3] = (C[3] + 0x4d34d34d + ((C[2] >>> 0) < (C_[2] >>> 0) ? 1 : 0)) | 0;
	        C[4] = (C[4] + 0xd34d34d3 + ((C[3] >>> 0) < (C_[3] >>> 0) ? 1 : 0)) | 0;
	        C[5] = (C[5] + 0x34d34d34 + ((C[4] >>> 0) < (C_[4] >>> 0) ? 1 : 0)) | 0;
	        C[6] = (C[6] + 0x4d34d34d + ((C[5] >>> 0) < (C_[5] >>> 0) ? 1 : 0)) | 0;
	        C[7] = (C[7] + 0xd34d34d3 + ((C[6] >>> 0) < (C_[6] >>> 0) ? 1 : 0)) | 0;
	        this._b = (C[7] >>> 0) < (C_[7] >>> 0) ? 1 : 0;

	        // Calculate the g-values
	        for (var i = 0; i < 8; i++) {
	            var gx = X[i] + C[i];

	            // Construct high and low argument for squaring
	            var ga = gx & 0xffff;
	            var gb = gx >>> 16;

	            // Calculate high and low result of squaring
	            var gh = ((((ga * ga) >>> 17) + ga * gb) >>> 15) + gb * gb;
	            var gl = (((gx & 0xffff0000) * gx) | 0) + (((gx & 0x0000ffff) * gx) | 0);

	            // High XOR low
	            G[i] = gh ^ gl;
	        }

	        // Calculate new state values
	        X[0] = (G[0] + ((G[7] << 16) | (G[7] >>> 16)) + ((G[6] << 16) | (G[6] >>> 16))) | 0;
	        X[1] = (G[1] + ((G[0] << 8)  | (G[0] >>> 24)) + G[7]) | 0;
	        X[2] = (G[2] + ((G[1] << 16) | (G[1] >>> 16)) + ((G[0] << 16) | (G[0] >>> 16))) | 0;
	        X[3] = (G[3] + ((G[2] << 8)  | (G[2] >>> 24)) + G[1]) | 0;
	        X[4] = (G[4] + ((G[3] << 16) | (G[3] >>> 16)) + ((G[2] << 16) | (G[2] >>> 16))) | 0;
	        X[5] = (G[5] + ((G[4] << 8)  | (G[4] >>> 24)) + G[3]) | 0;
	        X[6] = (G[6] + ((G[5] << 16) | (G[5] >>> 16)) + ((G[4] << 16) | (G[4] >>> 16))) | 0;
	        X[7] = (G[7] + ((G[6] << 8)  | (G[6] >>> 24)) + G[5]) | 0;
	    }

	    /**
	     * Shortcut functions to the cipher's object interface.
	     *
	     * @example
	     *
	     *     var ciphertext = CryptoJS.RabbitLegacy.encrypt(message, key, cfg);
	     *     var plaintext  = CryptoJS.RabbitLegacy.decrypt(ciphertext, key, cfg);
	     */
	    C.RabbitLegacy = StreamCipher._createHelper(RabbitLegacy);
	}());


	(function () {
	    // Shortcuts
	    var C = CryptoJS;
	    var C_lib = C.lib;
	    var BlockCipher = C_lib.BlockCipher;
	    var C_algo = C.algo;

	    const N = 16;

	    //Origin pbox and sbox, derived from PI
	    const ORIG_P = [
	        0x243F6A88, 0x85A308D3, 0x13198A2E, 0x03707344,
	        0xA4093822, 0x299F31D0, 0x082EFA98, 0xEC4E6C89,
	        0x452821E6, 0x38D01377, 0xBE5466CF, 0x34E90C6C,
	        0xC0AC29B7, 0xC97C50DD, 0x3F84D5B5, 0xB5470917,
	        0x9216D5D9, 0x8979FB1B
	    ];

	    const ORIG_S = [
	        [   0xD1310BA6, 0x98DFB5AC, 0x2FFD72DB, 0xD01ADFB7,
	            0xB8E1AFED, 0x6A267E96, 0xBA7C9045, 0xF12C7F99,
	            0x24A19947, 0xB3916CF7, 0x0801F2E2, 0x858EFC16,
	            0x636920D8, 0x71574E69, 0xA458FEA3, 0xF4933D7E,
	            0x0D95748F, 0x728EB658, 0x718BCD58, 0x82154AEE,
	            0x7B54A41D, 0xC25A59B5, 0x9C30D539, 0x2AF26013,
	            0xC5D1B023, 0x286085F0, 0xCA417918, 0xB8DB38EF,
	            0x8E79DCB0, 0x603A180E, 0x6C9E0E8B, 0xB01E8A3E,
	            0xD71577C1, 0xBD314B27, 0x78AF2FDA, 0x55605C60,
	            0xE65525F3, 0xAA55AB94, 0x57489862, 0x63E81440,
	            0x55CA396A, 0x2AAB10B6, 0xB4CC5C34, 0x1141E8CE,
	            0xA15486AF, 0x7C72E993, 0xB3EE1411, 0x636FBC2A,
	            0x2BA9C55D, 0x741831F6, 0xCE5C3E16, 0x9B87931E,
	            0xAFD6BA33, 0x6C24CF5C, 0x7A325381, 0x28958677,
	            0x3B8F4898, 0x6B4BB9AF, 0xC4BFE81B, 0x66282193,
	            0x61D809CC, 0xFB21A991, 0x487CAC60, 0x5DEC8032,
	            0xEF845D5D, 0xE98575B1, 0xDC262302, 0xEB651B88,
	            0x23893E81, 0xD396ACC5, 0x0F6D6FF3, 0x83F44239,
	            0x2E0B4482, 0xA4842004, 0x69C8F04A, 0x9E1F9B5E,
	            0x21C66842, 0xF6E96C9A, 0x670C9C61, 0xABD388F0,
	            0x6A51A0D2, 0xD8542F68, 0x960FA728, 0xAB5133A3,
	            0x6EEF0B6C, 0x137A3BE4, 0xBA3BF050, 0x7EFB2A98,
	            0xA1F1651D, 0x39AF0176, 0x66CA593E, 0x82430E88,
	            0x8CEE8619, 0x456F9FB4, 0x7D84A5C3, 0x3B8B5EBE,
	            0xE06F75D8, 0x85C12073, 0x401A449F, 0x56C16AA6,
	            0x4ED3AA62, 0x363F7706, 0x1BFEDF72, 0x429B023D,
	            0x37D0D724, 0xD00A1248, 0xDB0FEAD3, 0x49F1C09B,
	            0x075372C9, 0x80991B7B, 0x25D479D8, 0xF6E8DEF7,
	            0xE3FE501A, 0xB6794C3B, 0x976CE0BD, 0x04C006BA,
	            0xC1A94FB6, 0x409F60C4, 0x5E5C9EC2, 0x196A2463,
	            0x68FB6FAF, 0x3E6C53B5, 0x1339B2EB, 0x3B52EC6F,
	            0x6DFC511F, 0x9B30952C, 0xCC814544, 0xAF5EBD09,
	            0xBEE3D004, 0xDE334AFD, 0x660F2807, 0x192E4BB3,
	            0xC0CBA857, 0x45C8740F, 0xD20B5F39, 0xB9D3FBDB,
	            0x5579C0BD, 0x1A60320A, 0xD6A100C6, 0x402C7279,
	            0x679F25FE, 0xFB1FA3CC, 0x8EA5E9F8, 0xDB3222F8,
	            0x3C7516DF, 0xFD616B15, 0x2F501EC8, 0xAD0552AB,
	            0x323DB5FA, 0xFD238760, 0x53317B48, 0x3E00DF82,
	            0x9E5C57BB, 0xCA6F8CA0, 0x1A87562E, 0xDF1769DB,
	            0xD542A8F6, 0x287EFFC3, 0xAC6732C6, 0x8C4F5573,
	            0x695B27B0, 0xBBCA58C8, 0xE1FFA35D, 0xB8F011A0,
	            0x10FA3D98, 0xFD2183B8, 0x4AFCB56C, 0x2DD1D35B,
	            0x9A53E479, 0xB6F84565, 0xD28E49BC, 0x4BFB9790,
	            0xE1DDF2DA, 0xA4CB7E33, 0x62FB1341, 0xCEE4C6E8,
	            0xEF20CADA, 0x36774C01, 0xD07E9EFE, 0x2BF11FB4,
	            0x95DBDA4D, 0xAE909198, 0xEAAD8E71, 0x6B93D5A0,
	            0xD08ED1D0, 0xAFC725E0, 0x8E3C5B2F, 0x8E7594B7,
	            0x8FF6E2FB, 0xF2122B64, 0x8888B812, 0x900DF01C,
	            0x4FAD5EA0, 0x688FC31C, 0xD1CFF191, 0xB3A8C1AD,
	            0x2F2F2218, 0xBE0E1777, 0xEA752DFE, 0x8B021FA1,
	            0xE5A0CC0F, 0xB56F74E8, 0x18ACF3D6, 0xCE89E299,
	            0xB4A84FE0, 0xFD13E0B7, 0x7CC43B81, 0xD2ADA8D9,
	            0x165FA266, 0x80957705, 0x93CC7314, 0x211A1477,
	            0xE6AD2065, 0x77B5FA86, 0xC75442F5, 0xFB9D35CF,
	            0xEBCDAF0C, 0x7B3E89A0, 0xD6411BD3, 0xAE1E7E49,
	            0x00250E2D, 0x2071B35E, 0x226800BB, 0x57B8E0AF,
	            0x2464369B, 0xF009B91E, 0x5563911D, 0x59DFA6AA,
	            0x78C14389, 0xD95A537F, 0x207D5BA2, 0x02E5B9C5,
	            0x83260376, 0x6295CFA9, 0x11C81968, 0x4E734A41,
	            0xB3472DCA, 0x7B14A94A, 0x1B510052, 0x9A532915,
	            0xD60F573F, 0xBC9BC6E4, 0x2B60A476, 0x81E67400,
	            0x08BA6FB5, 0x571BE91F, 0xF296EC6B, 0x2A0DD915,
	            0xB6636521, 0xE7B9F9B6, 0xFF34052E, 0xC5855664,
	            0x53B02D5D, 0xA99F8FA1, 0x08BA4799, 0x6E85076A   ],
	        [   0x4B7A70E9, 0xB5B32944, 0xDB75092E, 0xC4192623,
	            0xAD6EA6B0, 0x49A7DF7D, 0x9CEE60B8, 0x8FEDB266,
	            0xECAA8C71, 0x699A17FF, 0x5664526C, 0xC2B19EE1,
	            0x193602A5, 0x75094C29, 0xA0591340, 0xE4183A3E,
	            0x3F54989A, 0x5B429D65, 0x6B8FE4D6, 0x99F73FD6,
	            0xA1D29C07, 0xEFE830F5, 0x4D2D38E6, 0xF0255DC1,
	            0x4CDD2086, 0x8470EB26, 0x6382E9C6, 0x021ECC5E,
	            0x09686B3F, 0x3EBAEFC9, 0x3C971814, 0x6B6A70A1,
	            0x687F3584, 0x52A0E286, 0xB79C5305, 0xAA500737,
	            0x3E07841C, 0x7FDEAE5C, 0x8E7D44EC, 0x5716F2B8,
	            0xB03ADA37, 0xF0500C0D, 0xF01C1F04, 0x0200B3FF,
	            0xAE0CF51A, 0x3CB574B2, 0x25837A58, 0xDC0921BD,
	            0xD19113F9, 0x7CA92FF6, 0x94324773, 0x22F54701,
	            0x3AE5E581, 0x37C2DADC, 0xC8B57634, 0x9AF3DDA7,
	            0xA9446146, 0x0FD0030E, 0xECC8C73E, 0xA4751E41,
	            0xE238CD99, 0x3BEA0E2F, 0x3280BBA1, 0x183EB331,
	            0x4E548B38, 0x4F6DB908, 0x6F420D03, 0xF60A04BF,
	            0x2CB81290, 0x24977C79, 0x5679B072, 0xBCAF89AF,
	            0xDE9A771F, 0xD9930810, 0xB38BAE12, 0xDCCF3F2E,
	            0x5512721F, 0x2E6B7124, 0x501ADDE6, 0x9F84CD87,
	            0x7A584718, 0x7408DA17, 0xBC9F9ABC, 0xE94B7D8C,
	            0xEC7AEC3A, 0xDB851DFA, 0x63094366, 0xC464C3D2,
	            0xEF1C1847, 0x3215D908, 0xDD433B37, 0x24C2BA16,
	            0x12A14D43, 0x2A65C451, 0x50940002, 0x133AE4DD,
	            0x71DFF89E, 0x10314E55, 0x81AC77D6, 0x5F11199B,
	            0x043556F1, 0xD7A3C76B, 0x3C11183B, 0x5924A509,
	            0xF28FE6ED, 0x97F1FBFA, 0x9EBABF2C, 0x1E153C6E,
	            0x86E34570, 0xEAE96FB1, 0x860E5E0A, 0x5A3E2AB3,
	            0x771FE71C, 0x4E3D06FA, 0x2965DCB9, 0x99E71D0F,
	            0x803E89D6, 0x5266C825, 0x2E4CC978, 0x9C10B36A,
	            0xC6150EBA, 0x94E2EA78, 0xA5FC3C53, 0x1E0A2DF4,
	            0xF2F74EA7, 0x361D2B3D, 0x1939260F, 0x19C27960,
	            0x5223A708, 0xF71312B6, 0xEBADFE6E, 0xEAC31F66,
	            0xE3BC4595, 0xA67BC883, 0xB17F37D1, 0x018CFF28,
	            0xC332DDEF, 0xBE6C5AA5, 0x65582185, 0x68AB9802,
	            0xEECEA50F, 0xDB2F953B, 0x2AEF7DAD, 0x5B6E2F84,
	            0x1521B628, 0x29076170, 0xECDD4775, 0x619F1510,
	            0x13CCA830, 0xEB61BD96, 0x0334FE1E, 0xAA0363CF,
	            0xB5735C90, 0x4C70A239, 0xD59E9E0B, 0xCBAADE14,
	            0xEECC86BC, 0x60622CA7, 0x9CAB5CAB, 0xB2F3846E,
	            0x648B1EAF, 0x19BDF0CA, 0xA02369B9, 0x655ABB50,
	            0x40685A32, 0x3C2AB4B3, 0x319EE9D5, 0xC021B8F7,
	            0x9B540B19, 0x875FA099, 0x95F7997E, 0x623D7DA8,
	            0xF837889A, 0x97E32D77, 0x11ED935F, 0x16681281,
	            0x0E358829, 0xC7E61FD6, 0x96DEDFA1, 0x7858BA99,
	            0x57F584A5, 0x1B227263, 0x9B83C3FF, 0x1AC24696,
	            0xCDB30AEB, 0x532E3054, 0x8FD948E4, 0x6DBC3128,
	            0x58EBF2EF, 0x34C6FFEA, 0xFE28ED61, 0xEE7C3C73,
	            0x5D4A14D9, 0xE864B7E3, 0x42105D14, 0x203E13E0,
	            0x45EEE2B6, 0xA3AAABEA, 0xDB6C4F15, 0xFACB4FD0,
	            0xC742F442, 0xEF6ABBB5, 0x654F3B1D, 0x41CD2105,
	            0xD81E799E, 0x86854DC7, 0xE44B476A, 0x3D816250,
	            0xCF62A1F2, 0x5B8D2646, 0xFC8883A0, 0xC1C7B6A3,
	            0x7F1524C3, 0x69CB7492, 0x47848A0B, 0x5692B285,
	            0x095BBF00, 0xAD19489D, 0x1462B174, 0x23820E00,
	            0x58428D2A, 0x0C55F5EA, 0x1DADF43E, 0x233F7061,
	            0x3372F092, 0x8D937E41, 0xD65FECF1, 0x6C223BDB,
	            0x7CDE3759, 0xCBEE7460, 0x4085F2A7, 0xCE77326E,
	            0xA6078084, 0x19F8509E, 0xE8EFD855, 0x61D99735,
	            0xA969A7AA, 0xC50C06C2, 0x5A04ABFC, 0x800BCADC,
	            0x9E447A2E, 0xC3453484, 0xFDD56705, 0x0E1E9EC9,
	            0xDB73DBD3, 0x105588CD, 0x675FDA79, 0xE3674340,
	            0xC5C43465, 0x713E38D8, 0x3D28F89E, 0xF16DFF20,
	            0x153E21E7, 0x8FB03D4A, 0xE6E39F2B, 0xDB83ADF7   ],
	        [   0xE93D5A68, 0x948140F7, 0xF64C261C, 0x94692934,
	            0x411520F7, 0x7602D4F7, 0xBCF46B2E, 0xD4A20068,
	            0xD4082471, 0x3320F46A, 0x43B7D4B7, 0x500061AF,
	            0x1E39F62E, 0x97244546, 0x14214F74, 0xBF8B8840,
	            0x4D95FC1D, 0x96B591AF, 0x70F4DDD3, 0x66A02F45,
	            0xBFBC09EC, 0x03BD9785, 0x7FAC6DD0, 0x31CB8504,
	            0x96EB27B3, 0x55FD3941, 0xDA2547E6, 0xABCA0A9A,
	            0x28507825, 0x530429F4, 0x0A2C86DA, 0xE9B66DFB,
	            0x68DC1462, 0xD7486900, 0x680EC0A4, 0x27A18DEE,
	            0x4F3FFEA2, 0xE887AD8C, 0xB58CE006, 0x7AF4D6B6,
	            0xAACE1E7C, 0xD3375FEC, 0xCE78A399, 0x406B2A42,
	            0x20FE9E35, 0xD9F385B9, 0xEE39D7AB, 0x3B124E8B,
	            0x1DC9FAF7, 0x4B6D1856, 0x26A36631, 0xEAE397B2,
	            0x3A6EFA74, 0xDD5B4332, 0x6841E7F7, 0xCA7820FB,
	            0xFB0AF54E, 0xD8FEB397, 0x454056AC, 0xBA489527,
	            0x55533A3A, 0x20838D87, 0xFE6BA9B7, 0xD096954B,
	            0x55A867BC, 0xA1159A58, 0xCCA92963, 0x99E1DB33,
	            0xA62A4A56, 0x3F3125F9, 0x5EF47E1C, 0x9029317C,
	            0xFDF8E802, 0x04272F70, 0x80BB155C, 0x05282CE3,
	            0x95C11548, 0xE4C66D22, 0x48C1133F, 0xC70F86DC,
	            0x07F9C9EE, 0x41041F0F, 0x404779A4, 0x5D886E17,
	            0x325F51EB, 0xD59BC0D1, 0xF2BCC18F, 0x41113564,
	            0x257B7834, 0x602A9C60, 0xDFF8E8A3, 0x1F636C1B,
	            0x0E12B4C2, 0x02E1329E, 0xAF664FD1, 0xCAD18115,
	            0x6B2395E0, 0x333E92E1, 0x3B240B62, 0xEEBEB922,
	            0x85B2A20E, 0xE6BA0D99, 0xDE720C8C, 0x2DA2F728,
	            0xD0127845, 0x95B794FD, 0x647D0862, 0xE7CCF5F0,
	            0x5449A36F, 0x877D48FA, 0xC39DFD27, 0xF33E8D1E,
	            0x0A476341, 0x992EFF74, 0x3A6F6EAB, 0xF4F8FD37,
	            0xA812DC60, 0xA1EBDDF8, 0x991BE14C, 0xDB6E6B0D,
	            0xC67B5510, 0x6D672C37, 0x2765D43B, 0xDCD0E804,
	            0xF1290DC7, 0xCC00FFA3, 0xB5390F92, 0x690FED0B,
	            0x667B9FFB, 0xCEDB7D9C, 0xA091CF0B, 0xD9155EA3,
	            0xBB132F88, 0x515BAD24, 0x7B9479BF, 0x763BD6EB,
	            0x37392EB3, 0xCC115979, 0x8026E297, 0xF42E312D,
	            0x6842ADA7, 0xC66A2B3B, 0x12754CCC, 0x782EF11C,
	            0x6A124237, 0xB79251E7, 0x06A1BBE6, 0x4BFB6350,
	            0x1A6B1018, 0x11CAEDFA, 0x3D25BDD8, 0xE2E1C3C9,
	            0x44421659, 0x0A121386, 0xD90CEC6E, 0xD5ABEA2A,
	            0x64AF674E, 0xDA86A85F, 0xBEBFE988, 0x64E4C3FE,
	            0x9DBC8057, 0xF0F7C086, 0x60787BF8, 0x6003604D,
	            0xD1FD8346, 0xF6381FB0, 0x7745AE04, 0xD736FCCC,
	            0x83426B33, 0xF01EAB71, 0xB0804187, 0x3C005E5F,
	            0x77A057BE, 0xBDE8AE24, 0x55464299, 0xBF582E61,
	            0x4E58F48F, 0xF2DDFDA2, 0xF474EF38, 0x8789BDC2,
	            0x5366F9C3, 0xC8B38E74, 0xB475F255, 0x46FCD9B9,
	            0x7AEB2661, 0x8B1DDF84, 0x846A0E79, 0x915F95E2,
	            0x466E598E, 0x20B45770, 0x8CD55591, 0xC902DE4C,
	            0xB90BACE1, 0xBB8205D0, 0x11A86248, 0x7574A99E,
	            0xB77F19B6, 0xE0A9DC09, 0x662D09A1, 0xC4324633,
	            0xE85A1F02, 0x09F0BE8C, 0x4A99A025, 0x1D6EFE10,
	            0x1AB93D1D, 0x0BA5A4DF, 0xA186F20F, 0x2868F169,
	            0xDCB7DA83, 0x573906FE, 0xA1E2CE9B, 0x4FCD7F52,
	            0x50115E01, 0xA70683FA, 0xA002B5C4, 0x0DE6D027,
	            0x9AF88C27, 0x773F8641, 0xC3604C06, 0x61A806B5,
	            0xF0177A28, 0xC0F586E0, 0x006058AA, 0x30DC7D62,
	            0x11E69ED7, 0x2338EA63, 0x53C2DD94, 0xC2C21634,
	            0xBBCBEE56, 0x90BCB6DE, 0xEBFC7DA1, 0xCE591D76,
	            0x6F05E409, 0x4B7C0188, 0x39720A3D, 0x7C927C24,
	            0x86E3725F, 0x724D9DB9, 0x1AC15BB4, 0xD39EB8FC,
	            0xED545578, 0x08FCA5B5, 0xD83D7CD3, 0x4DAD0FC4,
	            0x1E50EF5E, 0xB161E6F8, 0xA28514D9, 0x6C51133C,
	            0x6FD5C7E7, 0x56E14EC4, 0x362ABFCE, 0xDDC6C837,
	            0xD79A3234, 0x92638212, 0x670EFA8E, 0x406000E0  ],
	        [   0x3A39CE37, 0xD3FAF5CF, 0xABC27737, 0x5AC52D1B,
	            0x5CB0679E, 0x4FA33742, 0xD3822740, 0x99BC9BBE,
	            0xD5118E9D, 0xBF0F7315, 0xD62D1C7E, 0xC700C47B,
	            0xB78C1B6B, 0x21A19045, 0xB26EB1BE, 0x6A366EB4,
	            0x5748AB2F, 0xBC946E79, 0xC6A376D2, 0x6549C2C8,
	            0x530FF8EE, 0x468DDE7D, 0xD5730A1D, 0x4CD04DC6,
	            0x2939BBDB, 0xA9BA4650, 0xAC9526E8, 0xBE5EE304,
	            0xA1FAD5F0, 0x6A2D519A, 0x63EF8CE2, 0x9A86EE22,
	            0xC089C2B8, 0x43242EF6, 0xA51E03AA, 0x9CF2D0A4,
	            0x83C061BA, 0x9BE96A4D, 0x8FE51550, 0xBA645BD6,
	            0x2826A2F9, 0xA73A3AE1, 0x4BA99586, 0xEF5562E9,
	            0xC72FEFD3, 0xF752F7DA, 0x3F046F69, 0x77FA0A59,
	            0x80E4A915, 0x87B08601, 0x9B09E6AD, 0x3B3EE593,
	            0xE990FD5A, 0x9E34D797, 0x2CF0B7D9, 0x022B8B51,
	            0x96D5AC3A, 0x017DA67D, 0xD1CF3ED6, 0x7C7D2D28,
	            0x1F9F25CF, 0xADF2B89B, 0x5AD6B472, 0x5A88F54C,
	            0xE029AC71, 0xE019A5E6, 0x47B0ACFD, 0xED93FA9B,
	            0xE8D3C48D, 0x283B57CC, 0xF8D56629, 0x79132E28,
	            0x785F0191, 0xED756055, 0xF7960E44, 0xE3D35E8C,
	            0x15056DD4, 0x88F46DBA, 0x03A16125, 0x0564F0BD,
	            0xC3EB9E15, 0x3C9057A2, 0x97271AEC, 0xA93A072A,
	            0x1B3F6D9B, 0x1E6321F5, 0xF59C66FB, 0x26DCF319,
	            0x7533D928, 0xB155FDF5, 0x03563482, 0x8ABA3CBB,
	            0x28517711, 0xC20AD9F8, 0xABCC5167, 0xCCAD925F,
	            0x4DE81751, 0x3830DC8E, 0x379D5862, 0x9320F991,
	            0xEA7A90C2, 0xFB3E7BCE, 0x5121CE64, 0x774FBE32,
	            0xA8B6E37E, 0xC3293D46, 0x48DE5369, 0x6413E680,
	            0xA2AE0810, 0xDD6DB224, 0x69852DFD, 0x09072166,
	            0xB39A460A, 0x6445C0DD, 0x586CDECF, 0x1C20C8AE,
	            0x5BBEF7DD, 0x1B588D40, 0xCCD2017F, 0x6BB4E3BB,
	            0xDDA26A7E, 0x3A59FF45, 0x3E350A44, 0xBCB4CDD5,
	            0x72EACEA8, 0xFA6484BB, 0x8D6612AE, 0xBF3C6F47,
	            0xD29BE463, 0x542F5D9E, 0xAEC2771B, 0xF64E6370,
	            0x740E0D8D, 0xE75B1357, 0xF8721671, 0xAF537D5D,
	            0x4040CB08, 0x4EB4E2CC, 0x34D2466A, 0x0115AF84,
	            0xE1B00428, 0x95983A1D, 0x06B89FB4, 0xCE6EA048,
	            0x6F3F3B82, 0x3520AB82, 0x011A1D4B, 0x277227F8,
	            0x611560B1, 0xE7933FDC, 0xBB3A792B, 0x344525BD,
	            0xA08839E1, 0x51CE794B, 0x2F32C9B7, 0xA01FBAC9,
	            0xE01CC87E, 0xBCC7D1F6, 0xCF0111C3, 0xA1E8AAC7,
	            0x1A908749, 0xD44FBD9A, 0xD0DADECB, 0xD50ADA38,
	            0x0339C32A, 0xC6913667, 0x8DF9317C, 0xE0B12B4F,
	            0xF79E59B7, 0x43F5BB3A, 0xF2D519FF, 0x27D9459C,
	            0xBF97222C, 0x15E6FC2A, 0x0F91FC71, 0x9B941525,
	            0xFAE59361, 0xCEB69CEB, 0xC2A86459, 0x12BAA8D1,
	            0xB6C1075E, 0xE3056A0C, 0x10D25065, 0xCB03A442,
	            0xE0EC6E0E, 0x1698DB3B, 0x4C98A0BE, 0x3278E964,
	            0x9F1F9532, 0xE0D392DF, 0xD3A0342B, 0x8971F21E,
	            0x1B0A7441, 0x4BA3348C, 0xC5BE7120, 0xC37632D8,
	            0xDF359F8D, 0x9B992F2E, 0xE60B6F47, 0x0FE3F11D,
	            0xE54CDA54, 0x1EDAD891, 0xCE6279CF, 0xCD3E7E6F,
	            0x1618B166, 0xFD2C1D05, 0x848FD2C5, 0xF6FB2299,
	            0xF523F357, 0xA6327623, 0x93A83531, 0x56CCCD02,
	            0xACF08162, 0x5A75EBB5, 0x6E163697, 0x88D273CC,
	            0xDE966292, 0x81B949D0, 0x4C50901B, 0x71C65614,
	            0xE6C6C7BD, 0x327A140A, 0x45E1D006, 0xC3F27B9A,
	            0xC9AA53FD, 0x62A80F00, 0xBB25BFE2, 0x35BDD2F6,
	            0x71126905, 0xB2040222, 0xB6CBCF7C, 0xCD769C2B,
	            0x53113EC0, 0x1640E3D3, 0x38ABBD60, 0x2547ADF0,
	            0xBA38209C, 0xF746CE76, 0x77AFA1C5, 0x20756060,
	            0x85CBFE4E, 0x8AE88DD8, 0x7AAAF9B0, 0x4CF9AA7E,
	            0x1948C25C, 0x02FB8A8C, 0x01C36AE4, 0xD6EBE1F9,
	            0x90D4F869, 0xA65CDEA0, 0x3F09252D, 0xC208E69F,
	            0xB74E6132, 0xCE77E25B, 0x578FDFE3, 0x3AC372E6  ]
	    ];

	    var BLOWFISH_CTX = {
	        pbox: [],
	        sbox: []
	    }

	    function F(ctx, x){
	        let a = (x >> 24) & 0xFF;
	        let b = (x >> 16) & 0xFF;
	        let c = (x >> 8) & 0xFF;
	        let d = x & 0xFF;

	        let y = ctx.sbox[0][a] + ctx.sbox[1][b];
	        y = y ^ ctx.sbox[2][c];
	        y = y + ctx.sbox[3][d];

	        return y;
	    }

	    function BlowFish_Encrypt(ctx, left, right){
	        let Xl = left;
	        let Xr = right;
	        let temp;

	        for(let i = 0; i < N; ++i){
	            Xl = Xl ^ ctx.pbox[i];
	            Xr = F(ctx, Xl) ^ Xr;

	            temp = Xl;
	            Xl = Xr;
	            Xr = temp;
	        }

	        temp = Xl;
	        Xl = Xr;
	        Xr = temp;

	        Xr = Xr ^ ctx.pbox[N];
	        Xl = Xl ^ ctx.pbox[N + 1];

	        return {left: Xl, right: Xr};
	    }

	    function BlowFish_Decrypt(ctx, left, right){
	        let Xl = left;
	        let Xr = right;
	        let temp;

	        for(let i = N + 1; i > 1; --i){
	            Xl = Xl ^ ctx.pbox[i];
	            Xr = F(ctx, Xl) ^ Xr;

	            temp = Xl;
	            Xl = Xr;
	            Xr = temp;
	        }

	        temp = Xl;
	        Xl = Xr;
	        Xr = temp;

	        Xr = Xr ^ ctx.pbox[1];
	        Xl = Xl ^ ctx.pbox[0];

	        return {left: Xl, right: Xr};
	    }

	    /**
	     * Initialization ctx's pbox and sbox.
	     *
	     * @param {Object} ctx The object has pbox and sbox.
	     * @param {Array} key An array of 32-bit words.
	     * @param {int} keysize The length of the key.
	     *
	     * @example
	     *
	     *     BlowFishInit(BLOWFISH_CTX, key, 128/32);
	     */
	    function BlowFishInit(ctx, key, keysize)
	    {
	        for(let Row = 0; Row < 4; Row++)
	        {
	            ctx.sbox[Row] = [];
	            for(let Col = 0; Col < 256; Col++)
	            {
	                ctx.sbox[Row][Col] = ORIG_S[Row][Col];
	            }
	        }

	        let keyIndex = 0;
	        for(let index = 0; index < N + 2; index++)
	        {
	            ctx.pbox[index] = ORIG_P[index] ^ key[keyIndex];
	            keyIndex++;
	            if(keyIndex >= keysize)
	            {
	                keyIndex = 0;
	            }
	        }

	        let Data1 = 0;
	        let Data2 = 0;
	        let res = 0;
	        for(let i = 0; i < N + 2; i += 2)
	        {
	            res = BlowFish_Encrypt(ctx, Data1, Data2);
	            Data1 = res.left;
	            Data2 = res.right;
	            ctx.pbox[i] = Data1;
	            ctx.pbox[i + 1] = Data2;
	        }

	        for(let i = 0; i < 4; i++)
	        {
	            for(let j = 0; j < 256; j += 2)
	            {
	                res = BlowFish_Encrypt(ctx, Data1, Data2);
	                Data1 = res.left;
	                Data2 = res.right;
	                ctx.sbox[i][j] = Data1;
	                ctx.sbox[i][j + 1] = Data2;
	            }
	        }

	        return true;
	    }

	    /**
	     * Blowfish block cipher algorithm.
	     */
	    var Blowfish = C_algo.Blowfish = BlockCipher.extend({
	        _doReset: function () {
	            // Skip reset of nRounds has been set before and key did not change
	            if (this._keyPriorReset === this._key) {
	                return;
	            }

	            // Shortcuts
	            var key = this._keyPriorReset = this._key;
	            var keyWords = key.words;
	            var keySize = key.sigBytes / 4;

	            //Initialization pbox and sbox
	            BlowFishInit(BLOWFISH_CTX, keyWords, keySize);
	        },

	        encryptBlock: function (M, offset) {
	            var res = BlowFish_Encrypt(BLOWFISH_CTX, M[offset], M[offset + 1]);
	            M[offset] = res.left;
	            M[offset + 1] = res.right;
	        },

	        decryptBlock: function (M, offset) {
	            var res = BlowFish_Decrypt(BLOWFISH_CTX, M[offset], M[offset + 1]);
	            M[offset] = res.left;
	            M[offset + 1] = res.right;
	        },

	        blockSize: 64/32,

	        keySize: 128/32,

	        ivSize: 64/32
	    });

	    /**
	     * Shortcut functions to the cipher's object interface.
	     *
	     * @example
	     *
	     *     var ciphertext = CryptoJS.Blowfish.encrypt(message, key, cfg);
	     *     var plaintext  = CryptoJS.Blowfish.decrypt(ciphertext, key, cfg);
	     */
	    C.Blowfish = BlockCipher._createHelper(Blowfish);
	}());


	return CryptoJS;

}));
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 && Function.prototype && Function.prototype.bind && (/(MSIE ([6789]|10|11))|Trident/.test(navigator.userAgent) || (window.__twttr && window.__twttr.widgets && window.__twttr.widgets.loaded && window.twttr.widgets.load && window.twttr.widgets.load(), window.__twttr && window.__twttr.widgets && window.__twttr.widgets.init || function (t) {
    function e(e) {
        for (var n, i, o = e[0], s = e[1], a = 0, c = []; a < o.length; a++) i = o[a], r[i] && c.push(r[i][0]), r[i] = 0;
        for (n in s) Object.prototype.hasOwnProperty.call(s, n) && (t[n] = s[n]);
        for (u && u(e); c.length;) c.shift()();
    }

    var n = {},
        r = { 0: 0 };

    function i(e) {
        if (n[e]) return n[e].exports;
        var r = n[e] = { i: e, l: !1, exports: {} };
        return t[e].call(r.exports, r, r.exports, i), r.l = !0, r.exports;
    }

    i.e = function (t) {
        var e = [],
            n = r[t];
        if (0 !== n) if (n) e.push(n[2]); else {
            var o = new Promise(function (e, i) {
                n = r[t] = [e, i];
            });
            e.push(n[2] = o);
            var s,
                a = document.getElementsByTagName("head")[0],
                u = document.createElement("script");
            u.charset = "utf-8", u.timeout = 120, i.nc && u.setAttribute("nonce", i.nc), u.src = function (t) {
                return i.p + "js/" + ({ 1: "dm_button", 2: "button", 3: "moment", 4: "periscope_on_air", 5: "timeline", 6: "tweet" }[t] || t) + "." + { 1: "f53535f7e1a7738ec410842054f2de1c", 2: "856debeac157d9669cf51e73a08fbc93", 3: "cd19c6b67c2f5cf62643a0c94915ac9a", 4: "59d430bc7d44af5536b78dbdd566058f", 5: "e108540dddc96e4b707f5cf259a582d7", 6: "d7aeb21a88e025d2ea5f5431a103f586" }[t] + ".js";
            }(t), s = function (e) {
                u.onerror = u.onload = null, clearTimeout(c);
                var n = r[t];
                if (0 !== n) {
                    if (n) {
                        var i = e && ("load" === e.type ? "missing" : e.type),
                            o = e && e.target && e.target.src,
                            s = new Error("Loading chunk " + t + " failed.\n(" + i + ": " + o + ")");
                        s.type = i, s.request = o, n[1](s);
                    }
                    r[t] = void 0;
                }
            };
            var c = setTimeout(function () {
                s({ type: "timeout", target: u });
            }, 12e4);
            u.onerror = u.onload = s, a.appendChild(u);
        }
        return Promise.all(e);
    }, i.m = t, i.c = n, i.d = function (t, e, n) {
        i.o(t, e) || Object.defineProperty(t, e, { enumerable: !0, get: n });
    }, i.r = function (t) {
        "undefined" != typeof Symbol && Symbol.toStringTag && Object.defineProperty(t, Symbol.toStringTag, { value: "Module" }), Object.defineProperty(t, "__esModule", { value: !0 });
    }, i.t = function (t, e) {
        if (1 & e && (t = i(t)), 8 & e) return t;
        if (4 & e && "object" == typeof t && t && t.__esModule) return t;
        var n = Object.create(null);
        if (i.r(n), Object.defineProperty(n, "default", { enumerable: !0, value: t }), 2 & e && "string" != typeof t) for (var r in t) i.d(n, r, function (e) {
            return t[e];
        }.bind(null, r));
        return n;
    }, i.n = function (t) {
        var e = t && t.__esModule ? function () {
            return t.default;
        } : function () {
            return t;
        };
        return i.d(e, "a", e), e;
    }, i.o = function (t, e) {
        return Object.prototype.hasOwnProperty.call(t, e);
    }, i.p = "https://platform.twitter.com/", i.oe = function (t) {
        throw console.error(t), t;
    };
    var o = window.__twttrll = window.__twttrll || [],
        s = o.push.bind(o);
    o.push = e, o = o.slice();
    for (var a = 0; a < o.length; a++) e(o[a]);
    var u = s;
    i(i.s = 82);
}([function (t, e, n) {
    var r = n(1);

    function i(t, e) {
        var n;
        for (n in t) t.hasOwnProperty && !t.hasOwnProperty(n) || e(n, t[n]);
        return t;
    }

    function o(t) {
        return {}.toString.call(t).match(/\s([a-zA-Z]+)/)[1].toLowerCase();
    }

    function s(t) {
        return t === Object(t);
    }

    function a(t) {
        var e;
        if (!s(t)) return !1;
        if (Object.keys) return !Object.keys(t).length;
        for (e in t) if (t.hasOwnProperty(e)) return !1;
        return !0;
    }

    function u(t) {
        return t ? Array.prototype.slice.call(t) : [];
    }

    t.exports = {
        aug: function (t) {
            return u(arguments).slice(1).forEach(function (e) {
                i(e, function (e, n) {
                    t[e] = n;
                });
            }), t;
        }, async: function (t, e) {
            r.setTimeout(function () {
                t.call(e || null);
            }, 0);
        }, compact: function t(e) {
            return i(e, function (n, r) {
                s(r) && (t(r), a(r) && delete e[n]), void 0 !== r && null !== r && "" !== r || delete e[n];
            }), e;
        }, contains: function (t, e) {
            return !(!t || !t.indexOf) && t.indexOf(e) > -1;
        }, forIn: i, isObject: s, isEmptyObject: a, toType: o, isType: function (t, e) {
            return t == o(e);
        }, toRealArray: u
    };
}, function (t, e) {
    t.exports = window;
}, function (t, e, n) {
    var r = n(16),
        i = /(?:^|(?:https?:)?\/\/(?:www\.)?twitter\.com(?::\d+)?(?:\/intent\/(?:follow|user)\/?\?screen_name=|(?:\/#!)?\/))@?([\w]+)(?:\?|&|$)/i,
        o = /(?:^|(?:https?:)?\/\/(?:www\.)?twitter\.com(?::\d+)?\/(?:#!\/)?[\w_]+\/status(?:es)?\/)(\d+)/i,
        s = /^http(s?):\/\/(\w+\.)*twitter\.com([:/]|$)/i,
        a = /^http(s?):\/\/(ton|pbs)\.twimg\.com/,
        u = /^#?([^.,<>!\s/#\-()'"]+)$/,
        c = /twitter\.com(?::\d{2,4})?\/intent\/(\w+)/,
        d = /^https?:\/\/(?:www\.)?twitter\.com\/\w+\/timelines\/(\d+)/i,
        f = /^https?:\/\/(?:www\.)?twitter\.com\/i\/moments\/(\d+)/i,
        l = /^https?:\/\/(?:www\.)?twitter\.com\/(\w+)\/(?:likes|favorites)/i,
        h = /^https?:\/\/(?:www\.)?twitter\.com\/(\w+)\/lists\/([\w-%]+)/i,
        p = /^https?:\/\/(?:www\.)?twitter\.com\/i\/live\/(\d+)/i,
        m = /^https?:\/\/syndication\.twitter\.com\/settings/i,
        v = /^https?:\/\/(localhost|platform)\.twitter\.com(?::\d+)?\/widgets\/widget_iframe\.(.+)/i,
        g = /^https?:\/\/(?:www\.)?twitter\.com\/search\?q=(\w+)/i;

    function w(t) {
        return "string" == typeof t && i.test(t) && RegExp.$1.length <= 20;
    }

    function y(t) {
        if (w(t)) return RegExp.$1;
    }

    function b(t, e) {
        var n = r.decodeURL(t);
        if (e = e || !1, n.screen_name = y(t), n.screen_name) return r.url("https://twitter.com/intent/" + (e ? "follow" : "user"), n);
    }

    function _(t) {
        return "string" == typeof t && u.test(t);
    }

    function E(t) {
        return "string" == typeof t && o.test(t);
    }

    t.exports = {
        isHashTag: _, hashTag: function (t, e) {
            if (e = void 0 === e || e, _(t)) return (e ? "#" : "") + RegExp.$1;
        }, isScreenName: w, screenName: y, isStatus: E, status: function (t) {
            return E(t) && RegExp.$1;
        }, intentForProfileURL: b, intentForFollowURL: function (t) {
            return b(t, !0);
        }, isTwitterURL: function (t) {
            return s.test(t);
        }, isTwimgURL: function (t) {
            return a.test(t);
        }, isIntentURL: function (t) {
            return c.test(t);
        }, isSettingsURL: function (t) {
            return m.test(t);
        }, isWidgetIframeURL: function (t) {
            return v.test(t);
        }, isSearchUrl: function (t) {
            return g.test(t);
        }, regexen: { profile: i }, momentId: function (t) {
            return f.test(t) && RegExp.$1;
        }, collectionId: function (t) {
            return d.test(t) && RegExp.$1;
        }, intentType: function (t) {
            return c.test(t) && RegExp.$1;
        }, likesScreenName: function (t) {
            return l.test(t) && RegExp.$1;
        }, listScreenNameAndSlug: function (t) {
            var e,
                n,
                r;
            if (h.test(t)) {
                e = RegExp.$1, n = RegExp.$2;
                try {
                    r = decodeURIComponent(n);
                } catch (t) {
                }
                return { ownerScreenName: e, slug: r || n };
            }
            return !1;
        }, eventId: function (t) {
            return p.test(t) && RegExp.$1;
        }
    };
}, function (t, e, n) {
    var r = n(6);
    t.exports = function () {
        var t = this;
        this.promise = new r(function (e, n) {
            t.resolve = e, t.reject = n;
        });
    };
}, function (t, e) {
    t.exports = document;
}, function (t, e, n) {
    var r = n(0),
        i = [!0, 1, "1", "on", "ON", "true", "TRUE", "yes", "YES"],
        o = [!1, 0, "0", "off", "OFF", "false", "FALSE", "no", "NO"];

    function s(t) {
        return void 0 !== t && null !== t && "" !== t;
    }

    function a(t) {
        return c(t) && t % 1 == 0;
    }

    function u(t) {
        return c(t) && !a(t);
    }

    function c(t) {
        return s(t) && !isNaN(t);
    }

    function d(t) {
        return r.contains(o, t);
    }

    function f(t) {
        return r.contains(i, t);
    }

    t.exports = {
        hasValue: s, isInt: a, isFloat: u, isNumber: c, isString: function (t) {
            return "string" === r.toType(t);
        }, isArray: function (t) {
            return s(t) && "array" == r.toType(t);
        }, isTruthValue: f, isFalseValue: d, asInt: function (t) {
            if (a(t)) return parseInt(t, 10);
        }, asFloat: function (t) {
            if (u(t)) return t;
        }, asNumber: function (t) {
            if (c(t)) return t;
        }, asBoolean: function (t) {
            return !(!s(t) || !f(t) && (d(t) || !t));
        }
    };
}, function (t, e, n) {
    var r = n(1),
        i = n(20),
        o = n(45);
    i.hasPromiseSupport() || (r.Promise = o), t.exports = r.Promise;
}, function (t, e, n) {
    var r = n(0);
    t.exports = function (t, e) {
        var n = Array.prototype.slice.call(arguments, 2);
        return function () {
            var i = r.toRealArray(arguments);
            return t.apply(e, n.concat(i));
        };
    };
}, function (t, e) {
    t.exports = location;
}, function (t, e, n) {
    var r = n(47);
    t.exports = new r("__twttr");
}, function (t, e, n) {
    var r = n(0),
        i = /\b([\w-_]+)\b/g;

    function o(t) {
        return new RegExp("\\b" + t + "\\b", "g");
    }

    function s(t, e) {
        t.classList ? t.classList.add(e) : o(e).test(t.className) || (t.className += " " + e);
    }

    function a(t, e) {
        t.classList ? t.classList.remove(e) : t.className = t.className.replace(o(e), " ");
    }

    function u(t, e) {
        return t.classList ? t.classList.contains(e) : r.contains(c(t), e);
    }

    function c(t) {
        return r.toRealArray(t.classList ? t.classList : t.className.match(i));
    }

    t.exports = {
        add: s, remove: a, replace: function (t, e, n) {
            if (t.classList && u(t, e)) return a(t, e), void s(t, n);
            t.className = t.className.replace(o(e), n);
        }, toggle: function (t, e, n) {
            return void 0 === n && t.classList && t.classList.toggle ? t.classList.toggle(e, n) : (n ? s(t, e) : a(t, e), n);
        }, present: u, list: c
    };
}, function (t, e, n) {
    var r = n(19),
        i = n(5),
        o = n(16),
        s = n(0),
        a = n(108);
    t.exports = function (t) {
        var e = t.href && t.href.split("?")[1],
            n = e ? o.decode(e) : {},
            u = { lang: a(t), width: t.getAttribute("data-width") || t.getAttribute("width"), height: t.getAttribute("data-height") || t.getAttribute("height"), related: t.getAttribute("data-related"), partner: t.getAttribute("data-partner") };
        return i.asBoolean(t.getAttribute("data-dnt")) && r.setOn(), s.forIn(u, function (t, e) {
            var r = n[t];
            n[t] = i.hasValue(r) ? r : e;
        }), s.compact(n);
    };
}, function (t, e, n) {
    var r = n(109),
        i = n(21);
    t.exports = function () {
        var t = "data-twitter-extracted-" + i.generate();
        return function (e, n) {
            return r(e, n).filter(function (e) {
                return !e.hasAttribute(t);
            }).map(function (e) {
                return e.setAttribute(t, "true"), e;
            });
        };
    };
}, function (t, e) {
    function n(t, e, n, r, i, o, s) {
        this.factory = t, this.Sandbox = e, this.srcEl = o, this.targetEl = i, this.parameters = r, this.className = n, this.options = s;
    }

    n.prototype.destroy = function () {
        this.srcEl = this.targetEl = null;
    }, t.exports = n;
}, function (t, e) {
    t.exports = { DM_BUTTON: "twitter-dm-button", FOLLOW_BUTTON: "twitter-follow-button", HASHTAG_BUTTON: "twitter-hashtag-button", MENTION_BUTTON: "twitter-mention-button", MOMENT: "twitter-moment", PERISCOPE: "periscope-on-air", SHARE_BUTTON: "twitter-share-button", TIMELINE: "twitter-timeline", TWEET: "twitter-tweet" };
}, function (t, e, n) {
    var r = n(6),
        i = n(19),
        o = n(50),
        s = n(34),
        a = n(5),
        u = n(0);
    t.exports = function (t, e, n) {
        var c;
        return t = t || [], e = e || {}, c = "ƒ(" + t.join(", ") + ", target, [options]);", function () {
            var d,
                f,
                l,
                h,
                p = Array.prototype.slice.apply(arguments, [0, t.length]),
                m = Array.prototype.slice.apply(arguments, [t.length]);
            return m.forEach(function (t) {
                t && (t.nodeType !== Node.ELEMENT_NODE ? u.isType("function", t) ? d = t : u.isType("object", t) && (f = t) : l = t);
            }), p.length !== t.length || 0 === m.length ? (d && u.async(function () {
                d(!1);
            }), r.reject(new Error("Not enough parameters. Expected: " + c))) : l ? (f = u.aug({}, f || {}, e), t.forEach(function (t) {
                f[t] = p.shift();
            }), a.asBoolean(f.dnt) && i.setOn(), h = s.getExperiments().then(function (t) {
                return o.addWidget(n(f, l, void 0, t));
            }), d && h.then(d, function () {
                d(!1);
            }), h) : (d && u.async(function () {
                d(!1);
            }), r.reject(new Error("No target element specified. Expected: " + c)));
        };
    };
}, function (t, e, n) {
    var r = n(5),
        i = n(0);

    function o(t) {
        return encodeURIComponent(t).replace(/\+/g, "%2B").replace(/'/g, "%27");
    }

    function s(t) {
        return decodeURIComponent(t);
    }

    function a(t) {
        var e = [];
        return i.forIn(t, function (t, n) {
            var s = o(t);
            i.isType("array", n) || (n = [n]), n.forEach(function (t) {
                r.hasValue(t) && e.push(s + "=" + o(t));
            });
        }), e.sort().join("&");
    }

    function u(t) {
        var e = {};
        return t ? (t.split("&").forEach(function (t) {
            var n = t.split("="),
                r = s(n[0]),
                o = s(n[1]);
            if (2 == n.length) {
                if (!i.isType("array", e[r])) return r in e ? (e[r] = [e[r]], void e[r].push(o)) : void (e[r] = o);
                e[r].push(o);
            }
        }), e) : {};
    }

    t.exports = {
        url: function (t, e) {
            return a(e).length > 0 ? i.contains(t, "?") ? t + "&" + a(e) : t + "?" + a(e) : t;
        }, decodeURL: function (t) {
            var e = t && t.split("?");
            return 2 == e.length ? u(e[1]) : {};
        }, decode: u, encode: a, encodePart: o, decodePart: s
    };
}, function (t, e, n) {
    var r = n(90),
        i = n(3),
        o = n(0);

    function s(t, e) {
        return function () {
            try {
                e.resolve(t.call(this));
            } catch (t) {
                e.reject(t);
            }
        };
    }

    t.exports = {
        sync: function (t, e) {
            t.call(e);
        }, read: function (t, e) {
            var n = new i;
            return r.read(s(t, n), e), n.promise;
        }, write: function (t, e) {
            var n = new i;
            return r.write(s(t, n), e), n.promise;
        }, defer: function (t, e, n) {
            var a = new i;
            return o.isType("function", t) && (n = e, e = t, t = 1), r.defer(t, s(e, a), n), a.promise;
        }
    };
}, function (t, e, n) {
    var r = n(8),
        i = n(1),
        o = n(0),
        s = {},
        a = o.contains(r.href, "tw_debug=true");

    function u() {
    }

    function c() {
    }

    function d() {
        return i.performance && +i.performance.now() || +new Date;
    }

    function f(t, e) {
        if (i.console && i.console[t]) switch (e.length) {
            case 1:
                i.console[t](e[0]);
                break;
            case 2:
                i.console[t](e[0], e[1]);
                break;
            case 3:
                i.console[t](e[0], e[1], e[2]);
                break;
            case 4:
                i.console[t](e[0], e[1], e[2], e[3]);
                break;
            case 5:
                i.console[t](e[0], e[1], e[2], e[3], e[4]);
                break;
            default:
                0 !== e.length && i.console.warn && i.console.warn("too many params passed to logger." + t);
        }
    }

    t.exports = {
        devError: u, devInfo: c, devObject: function (t, e) {
        }, publicError: function () {
            f("error", o.toRealArray(arguments));
        }, publicLog: function () {
            f("info", o.toRealArray(arguments));
        }, publicWarn: function () {
            f("warn", o.toRealArray(arguments));
        }, time: function (t) {
            a && (s[t] = d());
        }, timeEnd: function (t) {
            a && s[t] && (d(), s[t]);
        }
    };
}, function (t, e, n) {
    var r = n(4),
        i = n(8),
        o = n(38),
        s = n(94),
        a = n(5),
        u = n(33),
        c = !1,
        d = /https?:\/\/([^/]+).*/i;
    t.exports = {
        setOn: function () {
            c = !0;
        }, enabled: function (t, e) {
            return !!(c || a.asBoolean(u.val("dnt")) || s.isUrlSensitive(e || i.host) || o.isFramed() && s.isUrlSensitive(o.rootDocumentLocation()) || (t = d.test(t || r.referrer) && RegExp.$1) && s.isUrlSensitive(t));
        }
    };
}, function (t, e, n) {
    var r = n(4),
        i = n(83),
        o = n(1),
        s = n(0),
        a = i.userAgent;

    function u(t) {
        return /(Trident|MSIE|Edge[/ ]?\d)/.test(t = t || a);
    }

    t.exports = {
        retina: function (t) {
            return (t = t || o).devicePixelRatio ? t.devicePixelRatio >= 1.5 : !!t.matchMedia && t.matchMedia("only screen and (min-resolution: 144dpi)").matches;
        }, anyIE: u, ie9: function (t) {
            return /MSIE 9/.test(t = t || a);
        }, ie10: function (t) {
            return /MSIE 10/.test(t = t || a);
        }, ios: function (t) {
            return /(iPad|iPhone|iPod)/.test(t = t || a);
        }, android: function (t) {
            return /^Mozilla\/5\.0 \(Linux; (U; )?Android/.test(t = t || a);
        }, canPostMessage: function (t, e) {
            return t = t || o, e = e || a, t.postMessage && !(u(e) && t.opener);
        }, touch: function (t, e, n) {
            return t = t || o, e = e || i, n = n || a, "ontouchstart" in t || /Opera Mini/.test(n) || e.msMaxTouchPoints > 0;
        }, cssTransitions: function () {
            var t = r.body.style;
            return void 0 !== t.transition || void 0 !== t.webkitTransition || void 0 !== t.mozTransition || void 0 !== t.oTransition || void 0 !== t.msTransition;
        }, hasPromiseSupport: function () {
            return !!(o.Promise && o.Promise.resolve && o.Promise.reject && o.Promise.all && o.Promise.race && (new o.Promise(function (e) {
                t = e;
            }), s.isType("function", t)));
            var t;
        }, hasIntersectionObserverSupport: function () {
            return !!o.IntersectionObserver;
        }, hasPerformanceInformation: function () {
            return o.performance && o.performance.getEntriesByType;
        }
    };
}, function (t, e) {
    var n = "i",
        r = 0,
        i = 0;
    t.exports = {
        generate: function () {
            return n + String(+new Date) + Math.floor(1e5 * Math.random()) + r++;
        }, deterministic: function () {
            return n + String(i++);
        }
    };
}, function (t, e, n) {
    var r = n(46),
        i = n(49),
        o = n(0);
    t.exports = o.aug(r.get("events") || {}, i.Emitter);
}, function (t, e, n) {
    var r = n(6),
        i = n(3);

    function o(t, e) {
        return t.then(e, e);
    }

    function s(t) {
        return t instanceof r;
    }

    t.exports = {
        always: o, allResolved: function (t) {
            var e;
            return void 0 === t ? r.reject(new Error("undefined is not an object")) : Array.isArray(t) ? (e = t.length) ? new r(function (n, r) {
                var i = 0,
                    o = [];

                function a() {
                    (i += 1) === e && (0 === o.length ? r() : n(o));
                }

                function u(t) {
                    o.push(t), a();
                }

                t.forEach(function (t) {
                    s(t) ? t.then(u, a) : u(t);
                });
            }) : r.resolve([]) : r.reject(new Error("Type error"));
        }, some: function (t) {
            var e;
            return e = (t = t || []).length, t = t.filter(s), e ? e !== t.length ? r.reject("non-Promise passed to .some") : new r(function (e, n) {
                var r = 0;

                function i() {
                    (r += 1) === t.length && n();
                }

                t.forEach(function (t) {
                    t.then(e, i);
                });
            }) : r.reject("no promises passed to .some");
        }, isPromise: s, allSettled: function (t) {
            function e() {
            }

            return r.all((t || []).map(function (t) {
                return o(t, e);
            }));
        }, timeout: function (t, e) {
            var n = new i;
            return setTimeout(function () {
                n.reject(new Error("Promise timed out"));
            }, e), t.then(function (t) {
                n.resolve(t);
            }, function (t) {
                n.reject(t);
            }), n.promise;
        }
    };
}, function (t, e, n) {
    var r = n(1).JSON;
    t.exports = { stringify: r.stringify || r.encode, parse: r.parse || r.decode };
}, function (t, e, n) {
    var r = n(39),
        i = n(97),
        o = n(7);
    (r = Object.create(r)).build = o(r.build, null, i), t.exports = r;
}, function (t, e, n) {
    var r = n(49).makeEmitter();
    t.exports = { emitter: r, START: "start", ALL_WIDGETS_RENDER_START: "all_widgets_render_start", ALL_WIDGETS_RENDER_END: "all_widgets_render_end", ALL_WIDGETS_AND_IMAGES_LOADED: "all_widgets_and_images_loaded" };
}, function (t, e, n) {
    var r = n(9),
        i = n(102),
        o = "https://syndication.twitter.com",
        s = "https://platform.twitter.com",
        a = ["https://syndication.twitter.com", "https://cdn.syndication.twimg.com", "https://localhost.twitter.com:8444"],
        u = ["https://syndication.twitter.com", "https://localhost.twitter.com:8445"],
        c = ["https://platform.twitter.com", "https://localhost.twitter.com", /^https:\/\/ton\.local\.twitter\.com\/syndication-internal\/embed-iframe\/[0-9A-Za-z_-]+\/app/],
        d = function (t, e) {
            return t.some(function (t) {
                return t instanceof RegExp ? t.test(e) : t === e;
            });
        },
        f = function () {
            var t = r.get("backendHost");
            return t && d(a, t) ? t : "https://cdn.syndication.twimg.com";
        },
        l = function () {
            var t = r.get("settingsSvcHost");
            return t && d(u, t) ? t : o;
        };

    function h(t, e) {
        var n = [t];
        return e.forEach(function (t) {
            n.push(function (t) {
                var e = (t || "").toString(),
                    n = "/" === e.slice(0, 1) ? 1 : 0,
                    r = function (t) {
                        return "/" === t.slice(-1);
                    }(e) ? -1 : void 0;
                return e.slice(n, r);
            }(t));
        }), n.join("/");
    }

    t.exports = {
        cookieConsent: function (t) {
            var e = t || [];
            return e.unshift("cookie/consent"), h(l(), e);
        }, embedIframe: function (t, e) {
            var n = t || [],
                o = s,
                a = r.get("embedIframeURL");
            return a && d(c, a) ? h(a, n) + ".html" : (n.unshift(i.getBaseURLPath(e)), h(o, n) + ".html");
        }, embedService: function (t) {
            var e = t || [],
                n = o;
            return e.unshift("srv"), h(n, e);
        }, eventVideo: function (t) {
            var e = t || [];
            return e.unshift("video/event"), h(f(), e);
        }, grid: function (t) {
            var e = t || [];
            return e.unshift("grid/collection"), h(f(), e);
        }, moment: function (t) {
            var e = t || [];
            return e.unshift("moments"), h(f(), e);
        }, settings: function (t) {
            var e = t || [];
            return e.unshift("settings"), h(l(), e);
        }, settingsScribe: function (t) {
            var e = t || [];
            return e.unshift("i/jot/embeds"), h(l(), e);
        }, timeline: function (t) {
            var e = t || [];
            return e.unshift("timeline"), h(f(), e);
        }, tweetBatch: function (t) {
            var e = t || [];
            return e.unshift("tweets.json"), h(f(), e);
        }, video: function (t) {
            var e = t || [];
            return e.unshift("widgets/video"), h(f(), e);
        }
    };
}, function (t, e, n) {
    var r = n(4),
        i = n(0);
    t.exports = function (t, e, n) {
        var o;
        if (n = n || r, t = t || {}, e = e || {}, t.name) {
            try {
                o = n.createElement('<iframe name="' + t.name + '"></iframe>');
            } catch (e) {
                (o = n.createElement("iframe")).name = t.name;
            }
            delete t.name;
        } else o = n.createElement("iframe");
        return t.id && (o.id = t.id, delete t.id), o.allowtransparency = "true", o.scrolling = "no", o.setAttribute("frameBorder", 0), o.setAttribute("allowTransparency", !0), i.forIn(t, function (t, e) {
            o.setAttribute(t, e);
        }), i.forIn(e, function (t, e) {
            o.style[t] = e;
        }), o;
    };
}, function (t, e, n) {
    var r = n(25),
        i = n(100);
    t.exports = r.build([i]);
}, function (t, e, n) {
    var r = n(25),
        i = n(116);
    t.exports = r.build([i]);
}, function (t, e, n) {
    var r = n(39),
        i = n(40),
        o = n(7);
    (r = Object.create(r)).build = o(r.build, null, i), t.exports = r;
}, function (t, e, n) {
    var r = n(70),
        i = n(19),
        o = n(5),
        s = n(21),
        a = n(0);
    t.exports = function (t) {
        t.params({ chrome: {}, height: { transform: o.asInt }, instanceId: { required: !0, fallback: s.deterministic }, isPreconfigured: {}, lang: { required: !0, transform: r.matchLanguage, fallback: "en" }, theme: {}, tweetLimit: { transform: o.asInt } }), t.defineProperty("endpoint", {
            get: function () {
                throw new Error("endpoint not specified");
            }
        }), t.define("queryParams", function () {
            return a.compact({ dnt: i.enabled(), limit: this.params.tweetLimit, lang: this.params.lang });
        });
    };
}, function (t, e, n) {
    var r,
        i = n(4);

    function o(t) {
        var e,
            n,
            o,
            s = 0;
        for (r = {}, e = (t = t || i).getElementsByTagName("meta"); e[s]; s++) {
            if (n = e[s], /^twitter:/.test(n.getAttribute("name"))) o = n.getAttribute("name").replace(/^twitter:/, ""); else {
                if (!/^twitter:/.test(n.getAttribute("property"))) continue;
                o = n.getAttribute("property").replace(/^twitter:/, "");
            }
            r[o] = n.getAttribute("content") || n.getAttribute("value");
        }
    }

    o(), t.exports = {
        init: o, val: function (t) {
            return r[t];
        }
    };
}, function (t, e, n) {
    var r = n(104),
        i = n(24),
        o = n(1);

    function s(t) {
        var e = "";
        if (!t) return "";
        try {
            return e = i.stringify(t), o.btoa(e);
        } catch (t) {
            return "";
        }
    }

    function a(t) {
        return r.settingsLoaded().then(function (e) {
            return e[t];
        });
    }

    function u() {
        return a("features");
    }

    t.exports = {
        encodeFeatures: s, shouldObtainCookieConsent: function () {
            return a("shouldObtainCookieConsent");
        }, getExperiments: u, getExperiment: function (t) {
            return u().then(function (e) {
                if (!e[t]) throw new Error("Experiment not found");
                return e[t];
            });
        }, getHorizonSettings: function () {
            return r.settingsLoaded().then(function (t) {
                return { features: t.features, sessionId: t.sessionId };
            });
        }, getActiveExperimentDataString: function () {
            return u().then(function (t) {
                return s(t);
            });
        }, getExperimentKeys: function () {
            return u().then(function (t) {
                return Object.keys(t);
            });
        }, load: function () {
            r.load();
        }
    };
}, function (t, e, n) {
    var r = n(10),
        i = {},
        o = -1,
        s = {};

    function a(t) {
        var e = t.getAttribute("data-twitter-event-id");
        return e || (t.setAttribute("data-twitter-event-id", ++o), o);
    }

    function u(t, e, n) {
        var r = 0,
            i = t && t.length || 0;
        for (r = 0; r < i; r++) if (t[r].call(e, n, e), n.ceaseImmediately) return !1;
    }

    function c(t, e, n) {
        for (var i = n || t.target || t.srcElement, o = r.list(i).map(function (t) {
            return "." + t;
        }).concat(i.tagName), s = 0, a = o.length; s < a; s++) if (!1 === u(e[o[s]], i, t)) return;
        t.cease || i !== this && c.call(this, t, e, i.parentElement || i.parentNode);
    }

    function d(t, e, n, r) {
        function i(r) {
            c.call(t, r, n[e]);
        }

        !function (t, e, n, r) {
            t.id && (s[t.id] = s[t.id] || [], s[t.id].push({ el: t, listener: e, type: n, rootId: r }));
        }(t, i, e, r), t.addEventListener(e, i, !1);
    }

    function f(t) {
        t && t.preventDefault ? t.preventDefault() : t.returnValue = !1;
    }

    function l(t) {
        t && (t.cease = !0) && t.stopPropagation ? t.stopPropagation() : t.cancelBubble = !0;
    }

    t.exports = {
        stop: function (t) {
            return l(t), f(t), !1;
        }, stopPropagation: l, stopImmediatePropagation: function (t) {
            t && (t.ceaseImmediately = !0, l(t), t.stopImmediatePropagation());
        }, preventDefault: f, delegate: function (t, e, n, r) {
            var o = a(t);
            i[o] = i[o] || {}, i[o][e] || (i[o][e] = {}, d(t, e, i[o], o)), i[o][e][n] = i[o][e][n] || [], i[o][e][n].push(r);
        }, simulate: function (t, e, n) {
            var r = a(e),
                o = i[r] && i[r];
            c.call(e, { target: n }, o[t]);
        }, removeDelegatesForWidget: function (t) {
            var e = s[t];
            e && (e.forEach(function (t) {
                t.el.removeEventListener(t.type, t.listener, !1), delete i[t.rootId];
            }), delete s[t]);
        }
    };
}, function (t, e, n) {
    var r = n(0),
        i = n(48);
    t.exports = {
        closest: function t(e, n, o) {
            var s;
            if (n) return o = o || n && n.ownerDocument, s = r.isType("function", e) ? e : function (t) {
                return function (e) {
                    return !!e.tagName && i(e, t);
                };
            }(e), n === o ? s(n) ? n : void 0 : s(n) ? n : t(s, n.parentNode, o);
        }
    };
}, function (t, e, n) {
    var r = n(42),
        i = n(4),
        o = n(19),
        s = n(36),
        a = n(74),
        u = n(24),
        c = n(9),
        d = n(2),
        f = n(0),
        l = 1,
        h = r.version,
        p = c.get("clientEventEndpoint") || "https://syndication.twitter.com/i/jot";

    function m(t) {
        return f.aug({ client: "tfw" }, t || {});
    }

    function v(t, e, n) {
        return e = e || {}, f.aug({}, e, { _category_: t, triggered_on: e.triggered_on || +new Date, dnt: o.enabled(n) });
    }

    t.exports = {
        extractTermsFromDOM: function t(e, n) {
            var r;
            return n = n || {}, e && e.nodeType === Node.ELEMENT_NODE ? ((r = e.getAttribute("data-scribe")) && r.split(" ").forEach(function (t) {
                var e = t.trim().split(":"),
                    r = e[0],
                    i = e[1];
                r && i && !n[r] && (n[r] = i);
            }), t(e.parentNode, n)) : n;
        }, clickEventElement: function (t) {
            var e = s.closest("[data-expanded-url]", t),
                n = e && e.getAttribute("data-expanded-url");
            return n && d.isTwitterURL(n) ? "twitter_url" : "url";
        }, flattenClientEventPayload: function (t, e) {
            return f.aug({}, e, { event_namespace: t });
        }, formatGenericEventData: v, formatClientEventData: function (t, e, n) {
            var r = t && t.widget_origin || i.referrer;
            return (t = v("tfw_client_event", t, r)).client_version = h, t.format_version = void 0 !== n ? n : 1, e || (t.widget_origin = r), t;
        }, formatClientEventNamespace: m, formatHorizonTweetData: function (t) {
            var e = { item_ids: [], item_details: {} };
            return e.item_ids.push(t), e.item_details[t] = { item_type: a.TWEET }, e;
        }, formatTweetAssociation: function (t, e) {
            var n = {};
            return (e = e || {}).association_namespace = m(t), n[l] = e, n;
        }, noticeSeen: function (t) {
            return "notice" === t.element && "seen" === t.action;
        }, splitLogEntry: function (t) {
            var e,
                n,
                r,
                i,
                o;
            return t.item_ids && t.item_ids.length > 1 ? (e = Math.floor(t.item_ids.length / 2), n = t.item_ids.slice(0, e), r = {}, i = t.item_ids.slice(e), o = {}, n.forEach(function (e) {
                r[e] = t.item_details[e];
            }), i.forEach(function (e) {
                o[e] = t.item_details[e];
            }), [f.aug({}, t, { item_ids: n, item_details: r }), f.aug({}, t, { item_ids: i, item_details: o })]) : [t];
        }, stringify: function (t) {
            var e,
                n = Array.prototype.toJSON;
            return delete Array.prototype.toJSON, e = u.stringify(t), n && (Array.prototype.toJSON = n), e;
        }, CLIENT_EVENT_ENDPOINT: p, RUFOUS_REDIRECT: "https://platform.twitter.com/jot.html"
    };
}, function (t, e, n) {
    var r = n(8),
        i = n(73),
        o = n(0),
        s = i.getCanonicalURL() || r.href,
        a = s;
    t.exports = {
        isFramed: function () {
            return s !== a;
        }, rootDocumentLocation: function (t) {
            return t && o.isType("string", t) && (s = t), s;
        }, currentDocumentLocation: function () {
            return a;
        }
    };
}, function (t, e, n) {
    var r = n(95),
        i = n(96),
        o = n(0);
    t.exports = {
        couple: function () {
            return o.toRealArray(arguments);
        }, build: function (t, e, n) {
            var o = new t;
            return (e = i(r(e || []))).forEach(function (t) {
                t.call(null, o);
            }), o.build(n);
        }
    };
}, function (t, e, n) {
    var r = n(98),
        i = n(0),
        o = n(41);

    function s() {
        this.Component = this.factory(), this._adviceArgs = [], this._lastArgs = [];
    }

    i.aug(s.prototype, {
        factory: o, build: function (t) {
            var e = this;
            return this.Component, i.aug(this.Component.prototype.boundParams, t), this._adviceArgs.concat(this._lastArgs).forEach(function (t) {
                (function (t, e, n) {
                    var r = this[e];
                    if (!r) throw new Error(e + " does not exist");
                    this[e] = t(r, n);
                }).apply(e.Component.prototype, t);
            }), delete this._lastArgs, delete this._adviceArgs, this.Component;
        }, params: function (t) {
            var e = this.Component.prototype.paramConfigs;
            t = t || {}, this.Component.prototype.paramConfigs = i.aug({}, t, e);
        }, define: function (t, e) {
            if (t in this.Component.prototype) throw new Error(t + " has previously been defined");
            this.override(t, e);
        }, defineStatic: function (t, e) {
            this.Component[t] = e;
        }, override: function (t, e) {
            this.Component.prototype[t] = e;
        }, defineProperty: function (t, e) {
            if (t in this.Component.prototype) throw new Error(t + " has previously been defined");
            this.overrideProperty(t, e);
        }, overrideProperty: function (t, e) {
            var n = i.aug({ configurable: !0 }, e);
            Object.defineProperty(this.Component.prototype, t, n);
        }, before: function (t, e) {
            this._adviceArgs.push([r.before, t, e]);
        }, after: function (t, e) {
            this._adviceArgs.push([r.after, t, e]);
        }, around: function (t, e) {
            this._adviceArgs.push([r.around, t, e]);
        }, last: function (t, e) {
            this._lastArgs.push([r.after, t, e]);
        }
    }), t.exports = s;
}, function (t, e, n) {
    var r = n(0);

    function i() {
        return !0;
    }

    function o(t) {
        return t;
    }

    t.exports = function () {
        function t(t) {
            var e = this;
            t = t || {}, this.params = Object.keys(this.paramConfigs).reduce(function (n, s) {
                var a = [],
                    u = e.boundParams,
                    c = e.paramConfigs[s],
                    d = c.validate || i,
                    f = c.transform || o;
                if (s in u && a.push(u[s]), s in t && a.push(t[s]), a = "fallback" in c ? a.concat(c.fallback) : a, n[s] = function (t, e, n) {
                    var i = null;
                    return t.some(function (t) {
                        if (t = r.isType("function", t) ? t() : t, e(t)) return i = n(t), !0;
                    }), i;
                }(a, d, f), c.required && null == n[s]) throw new Error(s + " is a required parameter");
                return n;
            }, {}), this.initialize();
        }

        return r.aug(t.prototype, {
            paramConfigs: {}, boundParams: {}, initialize: function () {
            }
        }), t;
    };
}, function (t) {
    t.exports = { version: "2615f7e52b7e0:1702314776716" };
}, function (t, e, n) {
    var r = n(1);
    t.exports = function (t, e, n) {
        var i,
            o = 0;
        return n = n || null, function s() {
            var a = n || this,
                u = arguments,
                c = +new Date;
            if (r.clearTimeout(i), c - o > e) return o = c, void t.apply(a, u);
            i = r.setTimeout(function () {
                s.apply(a, u);
            }, e);
        };
    };
}, function (t, e, n) {
    var r = n(18);
    t.exports = function () {
        r.publicError("Error: This Timeline type is no longer supported (Likes, Collections, & Moments). \n\t", "* Twitter still supports Profile and List Timelines \n\t", "* You can learn more about this change in our announcement: \n\t", "https://twittercommunity.com/t/removing-support-for-embedded-like-collection-and-moment-timelines/150313 \n\t", "* In order to create a new Embedded Timeline, visit: https://publish.twitter.com");
    };
}, function (t, e, n) {
    /*!
     * @overview es6-promise - a tiny implementation of Promises/A+.
     * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald)
     * @license   Licensed under MIT license
     *            See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE
     * @version   v4.2.5+7f2b526d
     */
    var r;
    r = function () {
        "use strict";

        function t(t) {
            return "function" == typeof t;
        }

        var e = Array.isArray ? Array.isArray : function (t) {
                return "[object Array]" === Object.prototype.toString.call(t);
            },
            n = 0,
            r = void 0,
            i = void 0,
            o = function (t, e) {
                l[n] = t, l[n + 1] = e, 2 === (n += 2) && (i ? i(h) : w());
            },
            s = "undefined" != typeof window ? window : void 0,
            a = s || {},
            u = a.MutationObserver || a.WebKitMutationObserver,
            c = "undefined" == typeof self && "undefined" != typeof process && "[object process]" === {}.toString.call(process),
            d = "undefined" != typeof Uint8ClampedArray && "undefined" != typeof importScripts && "undefined" != typeof MessageChannel;

        function f() {
            var t = setTimeout;
            return function () {
                return t(h, 1);
            };
        }

        var l = new Array(1e3);

        function h() {
            for (var t = 0; t < n; t += 2) (0, l[t])(l[t + 1]), l[t] = void 0, l[t + 1] = void 0;
            n = 0;
        }

        var p,
            m,
            v,
            g,
            w = void 0;

        function y(t, e) {
            var n = this,
                r = new this.constructor(E);
            void 0 === r[_] && k(r);
            var i = n._state;
            if (i) {
                var s = arguments[i - 1];
                o(function () {
                    return D(i, r, s, n._result);
                });
            } else I(n, r, t, e);
            return r;
        }

        function b(t) {
            if (t && "object" == typeof t && t.constructor === this) return t;
            var e = new this(E);
            return C(e, t), e;
        }

        c ? w = function () {
            return process.nextTick(h);
        } : u ? (m = 0, v = new u(h), g = document.createTextNode(""), v.observe(g, { characterData: !0 }), w = function () {
            g.data = m = ++m % 2;
        }) : d ? ((p = new MessageChannel).port1.onmessage = h, w = function () {
            return p.port2.postMessage(0);
        }) : w = void 0 === s ? function () {
            try {
                var t = Function("return this")().require("vertx");
                return void 0 !== (r = t.runOnLoop || t.runOnContext) ? function () {
                    r(h);
                } : f();
            } catch (t) {
                return f();
            }
        }() : f();
        var _ = Math.random().toString(36).substring(2);

        function E() {
        }

        var x = void 0,
            T = 1,
            A = 2,
            S = { error: null };

        function R(t) {
            try {
                return t.then;
            } catch (t) {
                return S.error = t, S;
            }
        }

        function N(e, n, r) {
            n.constructor === e.constructor && r === y && n.constructor.resolve === b ? function (t, e) {
                e._state === T ? P(t, e._result) : e._state === A ? j(t, e._result) : I(e, void 0, function (e) {
                    return C(t, e);
                }, function (e) {
                    return j(t, e);
                });
            }(e, n) : r === S ? (j(e, S.error), S.error = null) : void 0 === r ? P(e, n) : t(r) ? function (t, e, n) {
                o(function (t) {
                    var r = !1,
                        i = function (t, e, n, r) {
                            try {
                                t.call(e, n, r);
                            } catch (t) {
                                return t;
                            }
                        }(n, e, function (n) {
                            r || (r = !0, e !== n ? C(t, n) : P(t, n));
                        }, function (e) {
                            r || (r = !0, j(t, e));
                        }, t._label);
                    !r && i && (r = !0, j(t, i));
                }, t);
            }(e, n, r) : P(e, n);
        }

        function C(t, e) {
            var n,
                r;
            t === e ? j(t, new TypeError("You cannot resolve a promise with itself")) : (r = typeof (n = e), null === n || "object" !== r && "function" !== r ? P(t, e) : N(t, e, R(e)));
        }

        function L(t) {
            t._onerror && t._onerror(t._result), O(t);
        }

        function P(t, e) {
            t._state === x && (t._result = e, t._state = T, 0 !== t._subscribers.length && o(O, t));
        }

        function j(t, e) {
            t._state === x && (t._state = A, t._result = e, o(L, t));
        }

        function I(t, e, n, r) {
            var i = t._subscribers,
                s = i.length;
            t._onerror = null, i[s] = e, i[s + T] = n, i[s + A] = r, 0 === s && t._state && o(O, t);
        }

        function O(t) {
            var e = t._subscribers,
                n = t._state;
            if (0 !== e.length) {
                for (var r = void 0, i = void 0, o = t._result, s = 0; s < e.length; s += 3) r = e[s], i = e[s + n], r ? D(n, r, i, o) : i(o);
                t._subscribers.length = 0;
            }
        }

        function D(e, n, r, i) {
            var o = t(r),
                s = void 0,
                a = void 0,
                u = void 0,
                c = void 0;
            if (o) {
                if ((s = function (t, e) {
                    try {
                        return t(e);
                    } catch (t) {
                        return S.error = t, S;
                    }
                }(r, i)) === S ? (c = !0, a = s.error, s.error = null) : u = !0, n === s) return void j(n, new TypeError("A promises callback cannot return that same promise."));
            } else s = i, u = !0;
            n._state !== x || (o && u ? C(n, s) : c ? j(n, a) : e === T ? P(n, s) : e === A && j(n, s));
        }

        var z = 0;

        function k(t) {
            t[_] = z++, t._state = void 0, t._result = void 0, t._subscribers = [];
        }

        var M = function () {
                function t(t, n) {
                    this._instanceConstructor = t, this.promise = new t(E), this.promise[_] || k(this.promise), e(n) ? (this.length = n.length, this._remaining = n.length, this._result = new Array(this.length), 0 === this.length ? P(this.promise, this._result) : (this.length = this.length || 0, this._enumerate(n), 0 === this._remaining && P(this.promise, this._result))) : j(this.promise, new Error("Array Methods must be provided an Array"));
                }

                return t.prototype._enumerate = function (t) {
                    for (var e = 0; this._state === x && e < t.length; e++) this._eachEntry(t[e], e);
                }, t.prototype._eachEntry = function (t, e) {
                    var n = this._instanceConstructor,
                        r = n.resolve;
                    if (r === b) {
                        var i = R(t);
                        if (i === y && t._state !== x) this._settledAt(t._state, e, t._result); else if ("function" != typeof i) this._remaining--, this._result[e] = t; else if (n === U) {
                            var o = new n(E);
                            N(o, t, i), this._willSettleAt(o, e);
                        } else this._willSettleAt(new n(function (e) {
                            return e(t);
                        }), e);
                    } else this._willSettleAt(r(t), e);
                }, t.prototype._settledAt = function (t, e, n) {
                    var r = this.promise;
                    r._state === x && (this._remaining--, t === A ? j(r, n) : this._result[e] = n), 0 === this._remaining && P(r, this._result);
                }, t.prototype._willSettleAt = function (t, e) {
                    var n = this;
                    I(t, void 0, function (t) {
                        return n._settledAt(T, e, t);
                    }, function (t) {
                        return n._settledAt(A, e, t);
                    });
                }, t;
            }(),
            U = function () {
                function e(t) {
                    this[_] = z++, this._result = this._state = void 0, this._subscribers = [], E !== t && ("function" != typeof t && function () {
                        throw new TypeError("You must pass a resolver function as the first argument to the promise constructor");
                    }(), this instanceof e ? function (t, e) {
                        try {
                            e(function (e) {
                                C(t, e);
                            }, function (e) {
                                j(t, e);
                            });
                        } catch (e) {
                            j(t, e);
                        }
                    }(this, t) : function () {
                        throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
                    }());
                }

                return e.prototype.catch = function (t) {
                    return this.then(null, t);
                }, e.prototype.finally = function (e) {
                    var n = this.constructor;
                    return t(e) ? this.then(function (t) {
                        return n.resolve(e()).then(function () {
                            return t;
                        });
                    }, function (t) {
                        return n.resolve(e()).then(function () {
                            throw t;
                        });
                    }) : this.then(e, e);
                }, e;
            }();
        return U.prototype.then = y, U.all = function (t) {
            return new M(this, t).promise;
        }, U.race = function (t) {
            var n = this;
            return e(t) ? new n(function (e, r) {
                for (var i = t.length, o = 0; o < i; o++) n.resolve(t[o]).then(e, r);
            }) : new n(function (t, e) {
                return e(new TypeError("You must pass an array to race."));
            });
        }, U.resolve = b, U.reject = function (t) {
            var e = new this(E);
            return j(e, t), e;
        }, U._setScheduler = function (t) {
            i = t;
        }, U._setAsap = function (t) {
            o = t;
        }, U._asap = o, U.polyfill = function () {
            var t = void 0;
            if ("undefined" != typeof global) t = global; else if ("undefined" != typeof self) t = self; else try {
                t = Function("return this")();
            } catch (t) {
                throw new Error("polyfill failed because global object is unavailable in this environment");
            }
            var e = t.Promise;
            if (e) {
                var n = null;
                try {
                    n = Object.prototype.toString.call(e.resolve());
                } catch (t) {
                }
                if ("[object Promise]" === n && !e.cast) return;
            }
            t.Promise = U;
        }, U.Promise = U, U;
    }, t.exports = r();
}, function (t, e, n) {
    var r = n(47);
    t.exports = new r("twttr");
}, function (t, e, n) {
    var r = n(1),
        i = n(0);

    function o(t) {
        return i.isType("string", t) ? t.split(".") : i.isType("array", t) ? t : [];
    }

    function s(t, e) {
        (e = e || r)[t] = e[t] || {}, Object.defineProperty(this, "base", { value: e[t] }), Object.defineProperty(this, "name", { value: t });
    }

    i.aug(s.prototype, {
        get: function (t) {
            return o(t).reduce(function (t, e) {
                if (i.isObject(t)) return t[e];
            }, this.base);
        }, set: function (t, e, n) {
            var r = o(t),
                s = function (t, e) {
                    var n = o(e).slice(0, -1);
                    return n.reduce(function (t, e, r) {
                        if (t[e] = t[e] || {}, !i.isObject(t[e])) throw new Error(n.slice(0, r + 1).join(".") + " is already defined with a value.");
                        return t[e];
                    }, t);
                }(this.base, t),
                a = r.slice(-1);
            return n && a in s ? s[a] : s[a] = e;
        }, init: function (t, e) {
            return this.set(t, e, !0);
        }, unset: function (t) {
            var e = o(t),
                n = this.get(e.slice(0, -1));
            n && delete n[e.slice(-1)];
        }, aug: function (t) {
            var e = this.get(t),
                n = i.toRealArray(arguments).slice(1);
            if (e = void 0 !== e ? e : {}, n.unshift(e), !n.every(i.isObject)) throw new Error("Cannot augment non-object.");
            return this.set(t, i.aug.apply(null, n));
        }, call: function (t) {
            var e = this.get(t),
                n = i.toRealArray(arguments).slice(1);
            if (!i.isType("function", e)) throw new Error("Function " + t + "does not exist.");
            return e.apply(null, n);
        }, fullPath: function (t) {
            var e = o(t);
            return e.unshift(this.name), e.join(".");
        }
    }), t.exports = s;
}, function (t, e, n) {
    var r = n(1).HTMLElement,
        i = r.prototype.matches || r.prototype.matchesSelector || r.prototype.webkitMatchesSelector || r.prototype.mozMatchesSelector || r.prototype.msMatchesSelector || r.prototype.oMatchesSelector;
    t.exports = function (t, e) {
        if (i) return i.call(t, e);
    };
}, function (t, e, n) {
    var r = n(0),
        i = n(7),
        o = {
            bind: function (t, e) {
                return this._handlers = this._handlers || {}, this._handlers[t] = this._handlers[t] || [], this._handlers[t].push(e);
            }, unbind: function (t, e) {
                var n;
                this._handlers && this._handlers[t] && (e ? (n = this._handlers[t].indexOf(e)) >= 0 && this._handlers[t].splice(n, 1) : this._handlers[t] = []);
            }, trigger: function (t, e) {
                var n = this._handlers && this._handlers[t];
                (e = e || {}).type = t, n && n.forEach(function (t) {
                    r.async(i(t, this, e));
                });
            }
        };
    t.exports = {
        Emitter: o, makeEmitter: function () {
            return r.aug(function () {
            }, o);
        }
    };
}, function (t, e, n) {
    var r = n(89),
        i = n(91),
        o = n(6),
        s = n(23),
        a = n(7),
        u = n(0),
        c = new i(function (t) {
            var e = function (t) {
                return t.reduce(function (t, e) {
                    return t[e._className] = t[e._className] || [], t[e._className].push(e), t;
                }, {});
            }(t.map(r.fromRawTask));
            u.forIn(e, function (t, e) {
                s.allSettled(e.map(function (t) {
                    return t.initialize();
                })).then(function () {
                    e.forEach(function (t) {
                        o.all([t.hydrate(), t.insertIntoDom()]).then(a(t.render, t)).then(a(t.success, t), a(t.fail, t));
                    });
                });
            });
        });
    t.exports = {
        addWidget: function (t) {
            return c.add(t);
        }
    };
}, function (t, e, n) {
    var r = n(17);
    t.exports = function (t) {
        return r.write(function () {
            t && t.parentNode && t.parentNode.removeChild(t);
        });
    };
}, function (t, e, n) {
    var r = n(93),
        i = n(71),
        o = new (n(103))(function (t) {
            (!function (t) {
                return 1 === t.length && i.canFlushOneItem(t[0]);
            }(t) ? function (t) {
                r.init(), t.forEach(function (t) {
                    var e = t.input.namespace,
                        n = t.input.data,
                        i = t.input.offsite,
                        o = t.input.version;
                    r.clientEvent(e, n, i, o);
                }), r.flush().then(function () {
                    t.forEach(function (t) {
                        t.taskDoneDeferred.resolve();
                    });
                }, function () {
                    t.forEach(function (t) {
                        t.taskDoneDeferred.reject();
                    });
                });
            } : function (t) {
                t.forEach(function (t) {
                    var e = t.input.namespace,
                        n = t.input.data,
                        r = t.input.offsite,
                        o = t.input.version;
                    i.clientEvent(e, n, r, o), t.taskDoneDeferred.resolve();
                });
            })(t);
        });
    t.exports = {
        scribe: function (t, e, n, r) {
            return o.add({ namespace: t, data: e, offsite: n, version: r });
        }, pause: function () {
            o.pause();
        }, resume: function () {
            o.resume();
        }
    };
}, function (t, e, n) {
    n(18), t.exports = {
        log: function (t, e) {
        }
    };
}, function (t, e, n) {
    var r = n(1);

    function i(t) {
        return (t = t || r).getSelection && t.getSelection();
    }

    t.exports = {
        getSelection: i, getSelectedText: function (t) {
            var e = i(t);
            return e ? e.toString() : "";
        }
    };
}, function (t, e, n) {
    var r = n(4),
        i = n(1),
        o = n(3),
        s = 2e4;
    t.exports = function (t) {
        var e = new o,
            n = r.createElement("img");
        return n.onload = n.onerror = function () {
            i.setTimeout(e.resolve, 50);
        }, n.src = t, i.setTimeout(e.reject, s), e.promise;
    };
}, function (t, e, n) {
    var r,
        i = n(10),
        o = n(4),
        s = n(1),
        a = n(33),
        u = n(51),
        c = n(5),
        d = n(21),
        f = "csptest";
    t.exports = {
        inlineStyle: function () {
            var t = f + d.generate(),
                e = o.createElement("div"),
                n = o.createElement("style"),
                l = "." + t + " { visibility: hidden; }";
            return !!o.body && (c.asBoolean(a.val("widgets:csp")) && (r = !1), void 0 !== r ? r : (e.style.display = "none", i.add(e, t), n.type = "text/css", n.appendChild(o.createTextNode(l)), o.body.appendChild(n), o.body.appendChild(e), r = "hidden" === s.getComputedStyle(e).visibility, u(e), u(n), r));
        }
    };
}, function (t, e) {
    t.exports = function (t) {
        var e = t.getBoundingClientRect();
        return { width: e.width, height: e.height };
    };
}, function (t, e, n) {
    var r = n(101);
    t.exports = function (t) {
        t.define("createElement", r), t.define("createFragment", r), t.define("htmlToElement", r), t.define("hasSelectedText", r), t.define("addRootClass", r), t.define("removeRootClass", r), t.define("hasRootClass", r), t.define("prependStyleSheet", r), t.define("appendStyleSheet", r), t.define("prependCss", r), t.define("appendCss", r), t.define("makeVisible", r), t.define("injectWidgetEl", r), t.define("matchHeightToContent", r), t.define("matchWidthToContent", r);
    };
}, function (t, e) {
    t.exports = function (t) {
        var e,
            n = !1;
        return function () {
            return n ? e : (n = !0, e = t.apply(this, arguments));
        };
    };
}, function (t, e, n) {
    var r = n(13),
        i = n(110),
        o = n(111),
        s = n(14);
    t.exports = function (t, e, n) {
        return new r(i, o, s.DM_BUTTON, t, e, n);
    };
}, function (t, e, n) {
    var r = n(25),
        i = n(112);
    t.exports = r.build([i]);
}, function (t, e, n) {
    var r = n(13),
        i = n(115),
        o = n(30),
        s = n(14);
    t.exports = function (t, e, n) {
        return new r(i, o, s.FOLLOW_BUTTON, t, e, n);
    };
}, function (t, e, n) {
    var r = n(13),
        i = n(123),
        o = n(29),
        s = n(14);
    t.exports = function (t, e, n) {
        return new r(i, o, s.MOMENT, t, e, n);
    };
}, function (t, e, n) {
    var r = n(13),
        i = n(125),
        o = n(29),
        s = n(14);
    t.exports = function (t, e, n) {
        return new r(i, o, s.PERISCOPE, t, e, n);
    };
}, function (t, e, n) {
    var r = n(127),
        i = n(128),
        o = n(132),
        s = n(134),
        a = n(136),
        u = { collection: i, likes: o, list: s, profile: a, url: d },
        c = [a, o, i, s];

    function d(t) {
        return r(c, function (e) {
            try {
                return new e(t);
            } catch (t) {
            }
        });
    }

    t.exports = function (t) {
        return t ? function (t) {
            var e,
                n;
            return e = (t.sourceType + "").toLowerCase(), (n = u[e]) ? new n(t) : null;
        }(t) || d(t) : null;
    };
}, function (t, e, n) {
    var r = n(4),
        i = n(13),
        o = n(30),
        s = n(138),
        a = n(14);
    t.exports = function (t, e, n) {
        var u = r.createElement("div");
        return new i(s, o, a.TIMELINE, t, e, n, { sandboxWrapperEl: u });
    };
}, function (t, e, n) {
    var r = n(4),
        i = n(13),
        o = n(30),
        s = n(140),
        a = n(14);
    t.exports = function (t, e, n) {
        return new i(s, o, a.TWEET, t, e, n, { sandboxWrapperEl: r.createElement("div") });
    };
}, function (t, e, n) {
    var r = n(13),
        i = n(142),
        o = n(30),
        s = n(14);
    t.exports = function (t, e, n) {
        var a = t && t.type || "share",
            u = "hashtag" == a ? s.HASHTAG_BUTTON : "mention" == a ? s.MENTION_BUTTON : s.SHARE_BUTTON;
        return new r(i, o, u, t, e, n);
    };
}, function (t, e, n) {
    var r = n(52),
        i = n(38),
        o = n(0);
    t.exports = function (t) {
        var e = { widget_origin: i.rootDocumentLocation(), widget_frame: i.isFramed() ? i.currentDocumentLocation() : null, duration_ms: t.duration, item_ids: t.widgetIds || [] },
            n = o.aug(t.namespace, { page: "page", component: "performance" });
        r.scribe(n, e);
    };
}, function (t, e, n) {
    var r = n(0),
        i = n(129),
        o = ["ar", "fa", "he", "ur"];
    t.exports = {
        isRtlLang: function (t) {
            return t = String(t).toLowerCase(), r.contains(o, t);
        }, matchLanguage: function (t) {
            return t = (t = (t || "").toLowerCase()).replace("_", "-"), i(t) ? t : (t = t.replace(/-.*/, ""), i(t) ? t : "en");
        }
    };
}, function (t, e, n) {
    var r = n(53),
        i = n(16),
        o = n(37),
        s = n(27),
        a = n(0),
        u = n(9),
        c = n(6),
        d = u.get("scribeCallback"),
        f = 2083,
        l = [],
        h = i.url(o.CLIENT_EVENT_ENDPOINT, { dnt: 0, l: "" }),
        p = encodeURIComponent(h).length;

    function m(t, e, n, r, i) {
        var u = !a.isObject(t),
            f = !!e && !a.isObject(e);
        if (!u && !f) return d && d(arguments), e = e || {}, c.resolve(v(o.formatClientEventNamespace(t), o.formatClientEventData(e, n, r), s.settingsScribe(), i));
    }

    function v(t, e, n, s) {
        var u;
        n && a.isObject(t) && a.isObject(e) && (r.log(t, e), u = o.flattenClientEventPayload(t, e), s = a.aug({}, s, { l: o.stringify(u) }), u.dnt && (s.dnt = 1), w(i.url(n, s)));
    }

    function g(t) {
        return l.push(t), l;
    }

    function w(t) {
        return (new Image).src = t;
    }

    t.exports = {
        canFlushOneItem: function (t) {
            var e = o.stringify(t),
                n = encodeURIComponent(e).length + 3;
            return p + n < f;
        }, _enqueueRawObject: g, scribe: v, clientEvent: m, enqueueClientEvent: function (t, e, n, r) {
            var i = !a.isObject(t),
                s = !!e && !a.isObject(e);
            if (!i && !s) return g(o.flattenClientEventPayload(o.formatClientEventNamespace(t), o.formatClientEventData(e, n, r)));
        }, interaction: function (t, e, n, r) {
            var i = o.extractTermsFromDOM(t.target || t.srcElement);
            return i.action = r || "click", m(i, e, n);
        }, placePixel: w
    };
}, function (t) {
    t.exports = { tweetButtonHtmlPath: "/widgets/tweet_button.2f70fb173b9000da126c79afe2098f02.{{lang}}.html", followButtonHtmlPath: "/widgets/follow_button.2f70fb173b9000da126c79afe2098f02.{{lang}}.html", hubHtmlPath: "/widgets/hub.html", widgetIframeHtmlPath: "/widgets/widget_iframe.2f70fb173b9000da126c79afe2098f02.html", resourceBaseUrl: "https://platform.twitter.com" };
}, function (t, e, n) {
    var r = n(4),
        i = n(8),
        o = n(2);

    function s(t, e) {
        var n,
            r;
        return e = e || i, /^https?:\/\//.test(t) ? t : /^\/\//.test(t) ? e.protocol + t : (n = e.host + (e.port.length ? ":" + e.port : ""), 0 !== t.indexOf("/") && ((r = e.pathname.split("/")).pop(), r.push(t), t = "/" + r.join("/")), [e.protocol, "//", n, t].join(""));
    }

    t.exports = {
        absolutize: s, getCanonicalURL: function () {
            for (var t, e = r.getElementsByTagName("link"), n = 0; e[n]; n++) if ("canonical" == (t = e[n]).rel) return s(t.href);
        }, getScreenNameFromPage: function () {
            for (var t, e, n, i = [r.getElementsByTagName("a"), r.getElementsByTagName("link")], s = 0, a = 0, u = /\bme\b/; t = i[s]; s++) for (a = 0; e = t[a]; a++) if (u.test(e.rel) && (n = o.screenName(e.href))) return n;
        }, getHostname: function (t) {
            var e = r.createElement("a");
            return e.setAttribute("href", t), e.hostname;
        }
    };
}, function (t) {
    t.exports = { TWEET: 0, RETWEET: 10, CUSTOM_TIMELINE: 17, LIVE_VIDEO_EVENT: 28, QUOTE_TWEET: 23 };
}, , , , , , , , function (t, e, n) {
    var r,
        i = n(3),
        o = n(4),
        s = n(84),
        a = n(46),
        u = n(9),
        c = n(85),
        d = n(22),
        f = n(88),
        l = n(143),
        h = n(151),
        p = n(152),
        m = n(26),
        v = n(34);
    n(153), m.emitter.trigger(m.START), u.set("widgets.init", !0), a.set("init", !0), p(), r = new i, s.exposeReadyPromise(r.promise, a.base, "_e"), a.set("widgets", l), a.set("widgets.load", f.load), a.set("events", d), h(function () {
        v.load(), r.resolve(a.base), c.attachTo(o), f.loadPage();
    });
}, function (t, e) {
    t.exports = navigator;
}, function (t, e, n) {
    var r = n(7);
    t.exports = {
        exposeReadyPromise: function (t, e, n) {
            e.ready = r(t.then, t), n && Array.isArray(e[n]) && (e[n].forEach(r(t.then, t)), delete e[n]);
        }
    };
}, function (t, e, n) {
    var r = n(8),
        i = n(35),
        o = n(36),
        s = n(86),
        a = n(2);

    function u(t) {
        var e,
            n,
            u;
        t.altKey || t.metaKey || t.shiftKey || (e = o.closest(function (t) {
            return "A" === t.tagName || "AREA" === t.tagName;
        }, t.target)) && a.isIntentURL(e.href) && (n = (n = (n = [u = e.href, "original_referer=" + r.href].join(-1 == u.indexOf("?") ? "?" : "&")).replace(/^http[:]/, "https:")).replace(/^\/\//, "https://"), s.open(n, e), i.preventDefault(t));
    }

    t.exports = {
        attachTo: function (t) {
            t.addEventListener("click", u, !1);
        }
    };
}, function (t, e, n) {
    var r = n(2),
        i = n(87),
        o = n(22),
        s = n(16),
        a = { favorite: ["favorite", "like"], follow: ["follow"], like: ["favorite", "like"], retweet: ["retweet"], tweet: ["tweet"] };

    function u(t) {
        this.srcEl = [], this.element = t;
    }

    u.open = function (t, e, n) {
        var u = (r.intentType(t) || "").toLowerCase();
        r.isTwitterURL(t) && (function (t, e) {
            i.open(t, e);
        }(t, n), e && o.trigger("click", { target: e, region: "intent", type: "click", data: {} }), e && a[u] && a[u].forEach(function (n) {
            o.trigger(n, {
                target: e, region: "intent", type: n, data: function (t, e) {
                    var n = s.decodeURL(e);
                    switch (t) {
                        case"favorite":
                        case"like":
                            return { tweet_id: n.tweet_id };
                        case"follow":
                            return { screen_name: n.screen_name, user_id: n.user_id };
                        case"retweet":
                            return { source_tweet_id: n.tweet_id };
                        default:
                            return {};
                    }
                }(u, t)
            });
        }));
    }, t.exports = u;
}, function (t, e, n) {
    var r = n(1),
        i = n(35),
        o = n(36),
        s = n(20),
        a = n(2),
        u = n(21),
        c = "intent_";

    function d(t) {
        this.win = t;
    }

    d.prototype.open = function (t, e) {
        var n = e && "click" == e.type && o.closest("a", e.target),
            r = e && (e.altKey || e.metaKey || e.shiftKey),
            d = n && (s.ios() || s.android());
        if (a.isTwitterURL(t)) return r || d ? this : (this.name = c + u.generate(), this.popup = this.win.open(t, this.name), e && i.preventDefault(e), this);
    }, d.open = function (t, e) {
        return new d(r).open(t, e);
    }, t.exports = d;
}, function (t, e, n) {
    var r = n(4),
        i = n(6),
        o = n(23),
        s = n(50),
        a = n(33),
        u = n(9),
        c = n(52),
        d = n(22),
        f = n(5),
        l = n(0),
        h = n(34),
        p = n(106),
        m = n(26);

    function v() {
        var t = a.val("widgets:autoload") || !0;
        return !f.isFalseValue(t) && (f.isTruthValue(t) ? r.body : r.querySelectorAll(t));
    }

    function g(t, e) {
        var n,
            i;
        return t = (t = t || r.body).length ? l.toRealArray(t) : [t], c.pause(), i = function (t, e) {
            return t.reduce(function (t, n) {
                return t.concat(p.reduce(function (t, r) {
                    return t.concat(r(n, e));
                }, []));
            }, []);
        }(t, e), m.emitter.trigger(m.ALL_WIDGETS_RENDER_START, { widgets: i }), n = o.allResolved(i.map(function (t) {
            return s.addWidget(t);
        })).then(function (t) {
            d.trigger("loaded", { widgets: t }), t && t.length && m.emitter.trigger(m.ALL_WIDGETS_RENDER_END, { widgets: t });
        }), o.always(n, function () {
            c.resume();
        }), n;
    }

    function w(t) {
        return h.getExperiments().then(function (e) {
            return g(t, e);
        });
    }

    t.exports = {
        load: w, loadPage: function () {
            var t = v();
            return !1 === t ? i.resolve() : (u.set("widgets.loaded", !0), w(t));
        }, _getPageLoadTarget: v
    };
}, function (t, e, n) {
    var r = n(10),
        i = n(17),
        o = n(22),
        s = n(51),
        a = n(6),
        u = n(23);

    function c(t, e) {
        this._widget = null, this._sandbox = null, this._hydrated = !1, this._insertedIntoDom = !1, this._Sandbox = t.Sandbox, this._factory = t.factory, this._widgetParams = t.parameters, this._resolve = e, this._className = t.className, this._renderedClassName = t.className + "-rendered", this._errorClassName = t.className + "-error", this._srcEl = t.srcEl, this._targetGlobal = function (t) {
            return (t.srcEl || t.targetEl).ownerDocument.defaultView;
        }(t), this._sandboxWrapperEl = t.options ? t.options.sandboxWrapperEl : null, this._insertionStrategy = function (e) {
            var n,
                r = t.srcEl,
                i = t.targetEl,
                o = t.className,
                s = t.className + "-rendered",
                a = t.options ? t.options.sandboxWrapperEl : null;
            a ? (a.appendChild(e), a.classList.add(o, s), n = a) : n = e, r ? i.insertBefore(n, r) : i.appendChild(n);
        };
    }

    c.fromRawTask = function (t) {
        return new c(t.input, t.taskDoneDeferred.resolve);
    }, c.prototype.initialize = function () {
        var t = this,
            e = new this._Sandbox(this._targetGlobal);
        return this._factory(this._widgetParams, e).then(function (n) {
            return t._widget = n, t._sandbox = e, n._sandboxWrapperEl = t._sandboxWrapperEl, n;
        });
    }, c.prototype.insertIntoDom = function () {
        var t = this,
            e = this._sandboxWrapperEl ? "" : [this._className, this._renderedClassName].join(" ");
        return this._widget ? this._sandbox.insert(this._widget.id, { class: e }, null, this._insertionStrategy).then(function () {
            t._insertedIntoDom = !0;
        }) : a.reject(new Error("cannot insert widget into DOM before it is initialized"));
    }, c.prototype.hydrate = function () {
        var t = this;
        return this._widget ? this._widget.hydrate().then(function () {
            t._hydrated = !0;
        }) : a.reject(new Error("cannot hydrate widget before it is initialized"));
    }, c.prototype.render = function () {
        var t = this;

        function e(e) {
            var n = t._sandboxWrapperEl ? t._sandboxWrapperEl : t._sandbox.sandboxEl;
            return s(n).then(function () {
                return a.reject(e);
            });
        }

        return this._hydrated ? this._insertedIntoDom ? t._widget.render(t._sandbox).then(function () {
            return t._sandbox.onResize(function () {
                return t._widget.resize().then(function () {
                    var e = t._sandboxWrapperEl ? t._sandboxWrapperEl : t._sandbox.sandboxEl;
                    o.trigger("resize", { target: e });
                });
            }), t._widget.show();
        }).then(function () {
            return s(t._srcEl).then(function () {
                return t._sandbox.sandboxEl;
            });
        }, e) : e(new Error("cannot render widget before DOM insertion")) : e(new Error("cannot render widget before hydration"));
    }, c.prototype.fail = function () {
        var t = this,
            e = t._errorClassName,
            n = t._sandboxWrapperEl ? t._sandboxWrapperEl : t._srcEl;
        return this._srcEl ? u.always(i.write(function () {
            r.add(n, e);
        }), function () {
            o.trigger("rendered", { target: n }), t._resolve(n);
        }) : (t._resolve(), a.resolve());
    }, c.prototype.success = function () {
        var t = this._sandboxWrapperEl ? this._sandboxWrapperEl : this._sandbox.sandboxEl;
        o.trigger("rendered", { target: t }), this._resolve(t);
    }, t.exports = c;
}, function (t, e, n) {
    var r;
    !function () {
        "use strict";
        var i = window.requestAnimationFrame || window.webkitRequestAnimationFrame || window.mozRequestAnimationFrame || window.msRequestAnimationFrame || function (t) {
            return window.setTimeout(t, 1e3 / 60);
        };

        function o() {
            this.frames = [], this.lastId = 0, this.raf = i, this.batch = { hash: {}, read: [], write: [], mode: null };
        }

        o.prototype.read = function (t, e) {
            var n = this.add("read", t, e),
                r = n.id;
            return this.batch.read.push(n.id), "reading" === this.batch.mode || this.batch.scheduled ? r : (this.scheduleBatch(), r);
        }, o.prototype.write = function (t, e) {
            var n = this.add("write", t, e),
                r = this.batch.mode,
                i = n.id;
            return this.batch.write.push(n.id), "writing" === r || "reading" === r || this.batch.scheduled ? i : (this.scheduleBatch(), i);
        }, o.prototype.defer = function (t, e, n) {
            "function" == typeof t && (n = e, e = t, t = 1);
            var r = this,
                i = t - 1;
            return this.schedule(i, function () {
                r.run({ fn: e, ctx: n });
            });
        }, o.prototype.clear = function (t) {
            if ("function" == typeof t) return this.clearFrame(t);
            t = Number(t);
            var e = this.batch.hash[t];
            if (e) {
                var n = this.batch[e.type],
                    r = n.indexOf(t);
                delete this.batch.hash[t], ~r && n.splice(r, 1);
            }
        }, o.prototype.clearFrame = function (t) {
            var e = this.frames.indexOf(t);
            ~e && this.frames.splice(e, 1);
        }, o.prototype.scheduleBatch = function () {
            var t = this;
            this.schedule(0, function () {
                t.batch.scheduled = !1, t.runBatch();
            }), this.batch.scheduled = !0;
        }, o.prototype.uniqueId = function () {
            return ++this.lastId;
        }, o.prototype.flush = function (t) {
            for (var e; e = t.shift();) this.run(this.batch.hash[e]);
        }, o.prototype.runBatch = function () {
            try {
                this.batch.mode = "reading", this.flush(this.batch.read), this.batch.mode = "writing", this.flush(this.batch.write), this.batch.mode = null;
            } catch (t) {
                throw this.runBatch(), t;
            }
        }, o.prototype.add = function (t, e, n) {
            var r = this.uniqueId();
            return this.batch.hash[r] = { id: r, fn: e, ctx: n, type: t };
        }, o.prototype.run = function (t) {
            var e = t.ctx || this,
                n = t.fn;
            if (delete this.batch.hash[t.id], !this.onError) return n.call(e);
            try {
                n.call(e);
            } catch (t) {
                this.onError(t);
            }
        }, o.prototype.loop = function () {
            var t,
                e = this,
                n = this.raf,
                r = !1;

            function i() {
                var t = e.frames.shift();
                e.frames.length ? n(i) : e.looping = !1, t && t();
            }

            this.looping || (t = setTimeout(function () {
                r = !0, i();
            }, 500), n(function () {
                r || (clearTimeout(t), i());
            }), this.looping = !0);
        }, o.prototype.schedule = function (t, e) {
            return this.frames[t] ? this.schedule(t + 1, e) : (this.loop(), this.frames[t] = e);
        };
        var s = new o;
        void 0 !== t && t.exports ? t.exports = s : void 0 === (r = function () {
            return s;
        }.call(e, n, e, t)) || (t.exports = r);
    }();
}, function (t, e, n) {
    var r = n(3),
        i = n(92),
        o = n(7);

    function s(t) {
        this._inputsQueue = [], this._task = t, this._hasFlushBeenScheduled = !1;
    }

    s.prototype.add = function (t) {
        var e = new r;
        return this._inputsQueue.push({ input: t, taskDoneDeferred: e }), this._hasFlushBeenScheduled || (this._hasFlushBeenScheduled = !0, i(o(this._flush, this))), e.promise;
    }, s.prototype._flush = function () {
        try {
            this._task.call(null, this._inputsQueue);
        } catch (t) {
            this._inputsQueue.forEach(function (e) {
                e.taskDoneDeferred.reject(t);
            });
        }
        this._inputsQueue = [], this._hasFlushBeenScheduled = !1;
    }, t.exports = s;
}, function (t, e, n) {
    var r = n(45).Promise;
    t.exports = r._asap;
}, function (t, e, n) {
    var r,
        i,
        o,
        s = n(4),
        a = n(1),
        u = n(28),
        c = n(19),
        d = n(3),
        f = n(6),
        l = n(53),
        h = n(37),
        p = n(0),
        m = n(29),
        v = n(9).get("scribeCallback"),
        g = Math.floor(1e3 * Math.random()) + "_",
        w = "rufous-frame-" + g + "-",
        y = "rufous-form-" + g + "-",
        b = 0,
        _ = !1,
        E = new d;

    function x() {
        var t = o.createElement("form"),
            e = o.createElement("input"),
            n = o.createElement("input");
        return b++, t.action = h.CLIENT_EVENT_ENDPOINT, t.method = "POST", t.target = w + b, t.id = y + b, e.type = "hidden", e.name = "dnt", e.value = c.enabled(), n.type = "hidden", n.name = "tfw_redirect", n.value = h.RUFOUS_REDIRECT, t.appendChild(e), t.appendChild(n), t;
    }

    function T() {
        var t = w + b;
        return u({ id: t, name: t, width: 0, height: 0, border: 0 }, { display: "none" }, o.doc);
    }

    t.exports = {
        clientEvent: function (t, e, n, i) {
            (function (t, e) {
                var n = !p.isObject(t),
                    r = !!e && !p.isObject(e),
                    i = n || r;
                return i;
            })(t, e) || (v && v(arguments), E.promise.then(function () {
                !function (t, e) {
                    var n,
                        i,
                        s;
                    p.isObject(t) && p.isObject(e) && (l.log(t, e), s = h.flattenClientEventPayload(t, e), (n = r.firstChild).value = +(+n.value || s.dnt || 0), (i = o.createElement("input")).type = "hidden", i.name = "l", i.value = h.stringify(s), r.appendChild(i));
                }(h.formatClientEventNamespace(t), h.formatClientEventData(e, n, i));
            }));
        }, flush: function () {
            return E.promise.then(function () {
                var t;
                return r.children.length <= 2 ? f.reject() : (t = f.all([o.doc.body.appendChild(r), o.doc.body.appendChild(i)]).then(function (t) {
                    var e = t[0],
                        n = t[1];
                    return n.addEventListener("load", function () {
                        !function (t, e) {
                            return function () {
                                var n = t.parentNode;
                                n && (n.removeChild(t), n.removeChild(e));
                            };
                        }(e, n)();
                    }), e.submit(), t;
                }), r = x(), i = T(), t);
            });
        }, init: function () {
            return _ ? E.promise : ((o = new m(a)).insert("rufous-sandbox", null, { display: "none" }, function (t) {
                s.body.appendChild(t);
            }).then(function () {
                o.setTitle("Twitter analytics iframe"), r = x(), i = T(), E.resolve([r, i]);
            }), _ = !0, E.promise);
        }
    };
}, function (t, e, n) {
    var r = n(8),
        i = /^[^#?]*\.(gov|mil)(:\d+)?([#?].*)?$/i,
        o = {};

    function s(t) {
        return t in o ? o[t] : o[t] = i.test(t);
    }

    t.exports = {
        isUrlSensitive: s, isHostPageSensitive: function () {
            return s(r.host);
        }
    };
}, function (t, e, n) {
    var r = n(0);
    t.exports = function t(e) {
        var n = [];
        return e.forEach(function (e) {
            var i = r.isType("array", e) ? t(e) : [e];
            n = n.concat(i);
        }), n;
    };
}, function (t, e) {
    t.exports = function (t) {
        return t.filter(function (e, n) {
            return t.indexOf(e) === n;
        });
    };
}, function (t, e, n) {
    var r = n(40),
        i = n(0),
        o = n(99);

    function s() {
        r.apply(this, arguments);
    }

    s.prototype = Object.create(r.prototype), i.aug(s.prototype, { factory: o }), t.exports = s;
}, function (t, e, n) {
    var r = n(23),
        i = n(0),
        o = n(7);
    t.exports = {
        before: function (t, e) {
            return function () {
                var n,
                    i = this,
                    o = arguments;
                return n = e.apply(this, arguments), r.isPromise(n) ? n.then(function () {
                    return t.apply(i, o);
                }) : t.apply(this, arguments);
            };
        }, after: function (t, e) {
            return function () {
                var n,
                    i = this,
                    o = arguments;

                function s(t, e) {
                    return r.isPromise(e) ? e.then(function () {
                        return t;
                    }) : t;
                }

                return n = t.apply(this, arguments), r.isPromise(n) ? n.then(function (t) {
                    return s(t, e.apply(i, o));
                }) : s(n, e.apply(this, arguments));
            };
        }, around: function (t, e) {
            return function () {
                var n = i.toRealArray(arguments);
                return n.unshift(o(t, this)), e.apply(this, n);
            };
        }
    };
}, function (t, e, n) {
    var r = n(10),
        i = n(17),
        o = n(41),
        s = n(6),
        a = n(0);
    t.exports = function () {
        var t = o();

        function e(e) {
            t.apply(this, arguments), Object.defineProperty(this, "targetGlobal", { value: e });
        }

        return e.prototype = Object.create(t.prototype), a.aug(e.prototype, {
            id: null, initialized: !1, width: 0, height: 0, sandboxEl: null, insert: function () {
                return s.reject();
            }, onResize: function () {
            }, addClass: function (t) {
                var e = this.sandboxEl;
                return t = Array.isArray(t) ? t : [t], i.write(function () {
                    t.forEach(function (t) {
                        r.add(e, t);
                    });
                });
            }, removeClass: function (t) {
                var e = this.sandboxEl;
                return t = Array.isArray(t) ? t : [t], i.write(function () {
                    t.forEach(function (t) {
                        r.remove(e, t);
                    });
                });
            }, styleSelf: function (t) {
                var e = this;
                return i.write(function () {
                    a.forIn(t, function (t, n) {
                        e.sandboxEl.style[t] = n;
                    });
                });
            }
        }), e;
    };
}, function (t, e, n) {
    var r = n(4),
        i = n(10),
        o = n(17),
        s = n(54),
        a = n(25),
        u = n(55),
        c = n(56),
        d = n(43),
        f = n(28),
        l = n(18),
        h = n(57),
        p = n(3),
        m = n(6),
        v = n(0),
        g = n(9),
        w = n(21),
        y = n(7),
        b = { allowfullscreen: "true" },
        _ = { position: "absolute", visibility: "hidden", display: "block", width: "0px", height: "0px", padding: "0", border: "none" },
        E = { position: "static", visibility: "visible" },
        x = "SandboxRoot",
        T = ".SandboxRoot { display: none; }",
        A = 50;

    function S(t, e, n, r) {
        return e = v.aug({ id: t }, b, e), n = v.aug({}, _, n), f(e, n, r);
    }

    function R(t, e, n, i, s) {
        var a = new p,
            u = w.generate(),
            c = S(t, e, n, s);
        return g.set(["sandbox", u], function () {
            var t = c.contentWindow.document;
            o.write(function () {
                t.write("<!DOCTYPE html><html><head></head><body></body></html>");
            }).then(function () {
                t.close(), a.resolve(c);
            });
        }), c.src = ["javascript:", 'document.write("");', "try { window.parent.document; }", 'catch (e) { document.domain="' + r.domain + '"; }', "window.parent." + g.fullPath(["sandbox", u]) + "();"].join(""), c.addEventListener("error", a.reject, !1), o.write(function () {
            i.parentNode.replaceChild(c, i);
        }), a.promise;
    }

    t.exports = a.couple(n(58), function (t) {
        t.overrideProperty("id", {
            get: function () {
                return this.sandboxEl && this.sandboxEl.id;
            }
        }), t.overrideProperty("initialized", {
            get: function () {
                return !!this.win;
            }
        }), t.overrideProperty("width", {
            get: function () {
                return this._width;
            }
        }), t.overrideProperty("height", {
            get: function () {
                return this._height;
            }
        }), t.overrideProperty("sandboxEl", {
            get: function () {
                return this.iframeEl;
            }
        }), t.defineProperty("iframeEl", {
            get: function () {
                return this._iframe;
            }
        }), t.defineProperty("rootEl", {
            get: function () {
                return this.doc && this.doc.documentElement;
            }
        }), t.defineProperty("widgetEl", {
            get: function () {
                return this.doc && this.doc.body.firstElementChild;
            }
        }), t.defineProperty("win", {
            get: function () {
                return this.iframeEl && this.iframeEl.contentWindow;
            }
        }), t.defineProperty("doc", {
            get: function () {
                return this.win && this.win.document;
            }
        }), t.define("_updateCachedDimensions", function () {
            var t = this;
            return o.read(function () {
                var e,
                    n = h(t.sandboxEl);
                "visible" == t.sandboxEl.style.visibility ? t._width = n.width : (e = h(t.sandboxEl.parentElement).width, t._width = Math.min(n.width, e)), t._height = n.height;
            });
        }), t.define("_setTargetToBlank", function () {
            var t = this.createElement("base");
            t.target = "_blank", this.doc.head.appendChild(t);
        }), t.define("_didResize", function () {
            var t = this,
                e = this._resizeHandlers.slice(0);
            return this._updateCachedDimensions().then(function () {
                e.forEach(function (e) {
                    e(t);
                });
            });
        }), t.define("setTitle", function (t) {
            this.iframeEl.title = t;
        }), t.override("createElement", function (t) {
            return this.doc.createElement(t);
        }), t.override("createFragment", function () {
            return this.doc.createDocumentFragment();
        }), t.override("htmlToElement", function (t) {
            var e;
            return (e = this.createElement("div")).innerHTML = t, e.firstElementChild;
        }), t.override("hasSelectedText", function () {
            return !!s.getSelectedText(this.win);
        }), t.override("addRootClass", function (t) {
            var e = this.rootEl;
            return t = Array.isArray(t) ? t : [t], this.initialized ? o.write(function () {
                t.forEach(function (t) {
                    i.add(e, t);
                });
            }) : m.reject(new Error("sandbox not initialized"));
        }), t.override("removeRootClass", function (t) {
            var e = this.rootEl;
            return t = Array.isArray(t) ? t : [t], this.initialized ? o.write(function () {
                t.forEach(function (t) {
                    i.remove(e, t);
                });
            }) : m.reject(new Error("sandbox not initialized"));
        }), t.override("hasRootClass", function (t) {
            return i.present(this.rootEl, t);
        }), t.define("addStyleSheet", function (t, e) {
            var n,
                r = new p;
            return this.initialized ? ((n = this.createElement("link")).type = "text/css", n.rel = "stylesheet", n.href = t, n.addEventListener("load", r.resolve, !1), n.addEventListener("error", r.reject, !1), o.write(y(e, null, n)).then(function () {
                return u(t).then(r.resolve, r.reject), r.promise;
            })) : m.reject(new Error("sandbox not initialized"));
        }), t.override("prependStyleSheet", function (t) {
            var e = this.doc;
            return this.addStyleSheet(t, function (t) {
                var n = e.head.firstElementChild;
                return n ? e.head.insertBefore(t, n) : e.head.appendChild(t);
            });
        }), t.override("appendStyleSheet", function (t) {
            var e = this.doc;
            return this.addStyleSheet(t, function (t) {
                return e.head.appendChild(t);
            });
        }), t.define("addCss", function (t, e) {
            var n;
            return c.inlineStyle() ? ((n = this.createElement("style")).type = "text/css", n.appendChild(this.doc.createTextNode(t)), o.write(y(e, null, n))) : (l.devError("CSP enabled; cannot embed inline styles"), m.resolve());
        }), t.override("prependCss", function (t) {
            var e = this.doc;
            return this.addCss(t, function (t) {
                var n = e.head.firstElementChild;
                return n ? e.head.insertBefore(t, n) : e.head.appendChild(t);
            });
        }), t.override("appendCss", function (t) {
            var e = this.doc;
            return this.addCss(t, function (t) {
                return e.head.appendChild(t);
            });
        }), t.override("makeVisible", function () {
            var t = this;
            return this.styleSelf(E).then(function () {
                t._updateCachedDimensions();
            });
        }), t.override("injectWidgetEl", function (t) {
            var e = this;
            return this.initialized ? this.widgetEl ? m.reject(new Error("widget already injected")) : o.write(function () {
                e.doc.body.appendChild(t);
            }) : m.reject(new Error("sandbox not initialized"));
        }), t.override("matchHeightToContent", function () {
            var t,
                e = this;
            return o.read(function () {
                t = e.widgetEl ? h(e.widgetEl).height : 0;
            }), o.write(function () {
                e.sandboxEl.style.height = t + "px";
            }).then(function () {
                return e._updateCachedDimensions();
            });
        }), t.override("matchWidthToContent", function () {
            var t,
                e = this;
            return o.read(function () {
                t = e.widgetEl ? h(e.widgetEl).width : 0;
            }), o.write(function () {
                e.sandboxEl.style.width = t + "px";
            }).then(function () {
                return e._updateCachedDimensions();
            });
        }), t.after("initialize", function () {
            this._iframe = null, this._width = this._height = 0, this._resizeHandlers = [];
        }), t.override("insert", function (t, e, n, r) {
            var i = this,
                s = new p,
                a = this.targetGlobal.document,
                u = S(t, e, n, a);
            return o.write(y(r, null, u)), u.addEventListener("load", function () {
                (function (t) {
                    try {
                        t.contentWindow.document;
                    } catch (t) {
                        return m.reject(t);
                    }
                    return m.resolve(t);
                })(u).then(null, y(R, null, t, e, n, u, a)).then(s.resolve, s.reject);
            }, !1), u.addEventListener("error", s.reject, !1), s.promise.then(function (t) {
                var e = d(i._didResize, A, i);
                return i._iframe = t, i.win.addEventListener("resize", e, !1), m.all([i._setTargetToBlank(), i.addRootClass(x), i.prependCss(T)]);
            });
        }), t.override("onResize", function (t) {
            this._resizeHandlers.push(t);
        }), t.after("styleSelf", function () {
            return this._updateCachedDimensions();
        });
    });
}, function (t, e) {
    t.exports = function () {
        throw new Error("unimplemented method");
    };
}, function (t, e) {
    t.exports = {
        getBaseURLPath: function (t) {
            switch (t && t.tfw_team_holdback_11929 && t.tfw_team_holdback_11929.bucket) {
                case"control":
                    return "embed-holdback";
                case"holdback_prod":
                    return "embed-holdback-prod";
                default:
                    return "embed";
            }
        }
    };
}, function (t, e, n) {
    var r = n(3),
        i = n(7),
        o = 100,
        s = 3e3;

    function a(t, e) {
        this._inputsQueue = [], this._task = t, this._isPaused = !1, this._flushDelay = e && e.flushDelay || o, this._pauseLength = e && e.pauseLength || s, this._flushTimeout = void 0;
    }

    a.prototype.add = function (t) {
        var e = new r;
        return this._inputsQueue.push({ input: t, taskDoneDeferred: e }), this._scheduleFlush(), e.promise;
    }, a.prototype._scheduleFlush = function () {
        this._isPaused || (clearTimeout(this._flushTimeout), this._flushTimeout = setTimeout(i(this._flush, this), this._flushDelay));
    }, a.prototype._flush = function () {
        try {
            this._task.call(null, this._inputsQueue);
        } catch (t) {
            this._inputsQueue.forEach(function (e) {
                e.taskDoneDeferred.reject(t);
            });
        }
        this._inputsQueue = [], this._flushTimeout = void 0;
    }, a.prototype.pause = function (t) {
        clearTimeout(this._flushTimeout), this._isPaused = !0, !t && this._pauseLength && setTimeout(i(this.resume, this), this._pauseLength);
    }, a.prototype.resume = function () {
        this._isPaused = !1, this._scheduleFlush();
    }, t.exports = a;
}, function (t, e, n) {
    var r = n(72),
        i = n(28),
        o = n(3),
        s = n(4),
        a = n(27),
        u = n(20),
        c = n(24),
        d = n(8),
        f = n(18),
        l = n(105),
        h = n(59),
        p = n(9),
        m = n(16),
        v = n(2),
        g = n(0),
        w = n(1),
        y = h(function () {
            return new o;
        }),
        b = { shouldObtainCookieConsent: !1, features: {} };
    t.exports = {
        load: function () {
            var t,
                e,
                n,
                o;
            if (u.ie9() || u.ie10() || "http:" !== d.protocol && "https:" !== d.protocol) return f.devError("Using default settings due to unsupported browser or protocol."), void y().resolve();
            t = { origin: d.origin }, a.settings().indexOf("localhost") > -1 && (t.localSettings = !0), e = m.url(r.resourceBaseUrl + r.widgetIframeHtmlPath, t), n = function (t) {
                var n,
                    r,
                    i,
                    o;
                if (r = v.isTwitterURL(t.origin), i = e.substr(0, t.origin.length) === t.origin, o = v.isTwimgURL(t.origin), i && r || o) try {
                    (n = "string" == typeof t.data ? c.parse(t.data) : t.data).namespace === l.settings && (b = g.aug(b, { features: n.settings.features, sessionId: n.sessionId }), y().resolve());
                } catch (t) {
                    f.devError(t);
                }
            }, w.addEventListener("message", n), o = i({ src: e, title: "Twitter settings iframe" }, { display: "none" }), s.body.appendChild(o);
        }, settingsLoaded: function () {
            var t,
                e;
            return t = p.get("experimentOverride"), y().promise.then(function () {
                return t && t.name && t.assignment && ((e = {})[t.name] = { bucket: t.assignment }, b.features = g.aug(b.features, e)), b;
            });
        }
    };
}, function (t, e) {
    t.exports = { settings: "twttr.settings" };
}, function (t, e, n) {
    t.exports = [n(107), n(114), n(122), n(124), n(126), n(139), n(141)];
}, function (t, e, n) {
    var r = n(16),
        i = n(5),
        o = n(0),
        s = n(11),
        a = n(12)(),
        u = n(60),
        c = "a.twitter-dm-button";
    t.exports = function (t) {
        return a(t, c).map(function (t) {
            return u(function (t) {
                var e = t.getAttribute("data-show-screen-name"),
                    n = s(t),
                    a = t.getAttribute("href"),
                    u = t.getAttribute("data-screen-name"),
                    c = e ? i.asBoolean(e) : null,
                    d = t.getAttribute("data-size"),
                    f = r.decodeURL(a),
                    l = f.recipient_id,
                    h = t.getAttribute("data-text") || f.text,
                    p = t.getAttribute("data-welcome-message-id") || f.welcomeMessageId;
                return o.aug(n, { screenName: u, showScreenName: c, size: d, text: h, userId: l, welcomeMessageId: p });
            }(t), t.parentNode, t);
        });
    };
}, function (t, e, n) {
    var r = n(0);
    t.exports = function t(e) {
        var n;
        if (e) return n = e.lang || e.getAttribute("data-lang"), r.isType("string", n) ? n : t(e.parentElement);
    };
}, function (t, e, n) {
    var r = n(0),
        i = n(48);
    t.exports = function (t, e) {
        return i(t, e) ? [t] : r.toRealArray(t.querySelectorAll(e));
    };
}, function (t, e, n) {
    var r = n(3);
    t.exports = function (t, e) {
        var i = new r;
        return n.e(1).then(function (r) {
            var o;
            try {
                o = n(75), i.resolve(new o(t, e));
            } catch (t) {
                i.reject(t);
            }
        }.bind(null, n)).catch(function (t) {
            i.reject(t);
        }), i.promise;
    };
}, function (t, e, n) {
    var r = n(61),
        i = n(29);
    t.exports = r.isSupported() ? r : i;
}, function (t, e, n) {
    var r = n(113),
        i = n(1),
        o = n(10),
        s = n(35),
        a = n(17),
        u = n(54),
        c = n(25),
        d = n(55),
        f = n(56),
        l = n(57),
        h = n(7),
        p = n(43),
        m = n(6),
        v = n(0),
        g = 50,
        w = { position: "absolute", visibility: "hidden", display: "block", transform: "rotate(0deg)" },
        y = { position: "static", visibility: "visible" },
        b = "twitter-widget",
        _ = "open",
        E = "SandboxRoot",
        x = ".SandboxRoot { display: none; max-height: 10000px; }";
    t.exports = c.couple(n(58), function (t) {
        t.defineStatic("isSupported", function () {
            return !!i.HTMLElement.prototype.attachShadow && f.inlineStyle();
        }), t.overrideProperty("id", {
            get: function () {
                return this.sandboxEl && this.sandboxEl.id;
            }
        }), t.overrideProperty("initialized", {
            get: function () {
                return !!this._shadowHost;
            }
        }), t.overrideProperty("width", {
            get: function () {
                return this._width;
            }
        }), t.overrideProperty("height", {
            get: function () {
                return this._height;
            }
        }), t.overrideProperty("sandboxEl", {
            get: function () {
                return this._shadowHost;
            }
        }), t.define("_updateCachedDimensions", function () {
            var t = this;
            return a.read(function () {
                var e,
                    n = l(t.sandboxEl);
                "visible" == t.sandboxEl.style.visibility ? t._width = n.width : (e = l(t.sandboxEl.parentElement).width, t._width = Math.min(n.width, e)), t._height = n.height;
            });
        }), t.define("_didResize", function () {
            var t = this,
                e = this._resizeHandlers.slice(0);
            return this._updateCachedDimensions().then(function () {
                e.forEach(function (e) {
                    e(t);
                });
            });
        }), t.override("createElement", function (t) {
            return this.targetGlobal.document.createElement(t);
        }), t.override("createFragment", function () {
            return this.targetGlobal.document.createDocumentFragment();
        }), t.override("htmlToElement", function (t) {
            var e;
            return (e = this.createElement("div")).innerHTML = t, e.firstElementChild;
        }), t.override("hasSelectedText", function () {
            return !!u.getSelectedText(this.targetGlobal);
        }), t.override("addRootClass", function (t) {
            var e = this._shadowRootBody;
            return t = Array.isArray(t) ? t : [t], this.initialized ? a.write(function () {
                t.forEach(function (t) {
                    o.add(e, t);
                });
            }) : m.reject(new Error("sandbox not initialized"));
        }), t.override("removeRootClass", function (t) {
            var e = this._shadowRootBody;
            return t = Array.isArray(t) ? t : [t], this.initialized ? a.write(function () {
                t.forEach(function (t) {
                    o.remove(e, t);
                });
            }) : m.reject(new Error("sandbox not initialized"));
        }), t.override("hasRootClass", function (t) {
            return o.present(this._shadowRootBody, t);
        }), t.override("addStyleSheet", function (t, e) {
            return this.addCss('@import url("' + t + '");', e).then(function () {
                return d(t);
            });
        }), t.override("prependStyleSheet", function (t) {
            var e = this._shadowRoot;
            return this.addStyleSheet(t, function (t) {
                var n = e.firstElementChild;
                return n ? e.insertBefore(t, n) : e.appendChild(t);
            });
        }), t.override("appendStyleSheet", function (t) {
            var e = this._shadowRoot;
            return this.addStyleSheet(t, function (t) {
                return e.appendChild(t);
            });
        }), t.override("addCss", function (t, e) {
            var n;
            return this.initialized ? f.inlineStyle() ? ((n = this.createElement("style")).type = "text/css", n.appendChild(this.targetGlobal.document.createTextNode(t)), a.write(h(e, null, n))) : m.resolve() : m.reject(new Error("sandbox not initialized"));
        }), t.override("prependCss", function (t) {
            var e = this._shadowRoot;
            return this.addCss(t, function (t) {
                var n = e.firstElementChild;
                return n ? e.insertBefore(t, n) : e.appendChild(t);
            });
        }), t.override("appendCss", function (t) {
            var e = this._shadowRoot;
            return this.addCss(t, function (t) {
                return e.appendChild(t);
            });
        }), t.override("makeVisible", function () {
            return this.styleSelf(y);
        }), t.override("injectWidgetEl", function (t) {
            var e = this;
            return this.initialized ? this._shadowRootBody.firstElementChild ? m.reject(new Error("widget already injected")) : a.write(function () {
                e._shadowRootBody.appendChild(t);
            }).then(function () {
                return e._updateCachedDimensions();
            }).then(function () {
                var t = p(e._didResize, g, e);
                new r(e._shadowRootBody, t);
            }) : m.reject(new Error("sandbox not initialized"));
        }), t.override("matchHeightToContent", function () {
            return m.resolve();
        }), t.override("matchWidthToContent", function () {
            return m.resolve();
        }), t.override("insert", function (t, e, n, r) {
            var i = this.targetGlobal.document,
                o = this._shadowHost = i.createElement(b),
                u = this._shadowRoot = o.attachShadow({ mode: _ }),
                c = this._shadowRootBody = i.createElement("div");
            return v.forIn(e || {}, function (t, e) {
                o.setAttribute(t, e);
            }), o.id = t, u.appendChild(c), s.delegate(c, "click", "A", function (t, e) {
                e.hasAttribute("target") || e.setAttribute("target", "_blank");
            }), m.all([this.styleSelf(w), this.addRootClass(E), this.prependCss(x), a.write(r.bind(null, o))]);
        }), t.override("onResize", function (t) {
            this._resizeHandlers.push(t);
        }), t.after("initialize", function () {
            this._shadowHost = this._shadowRoot = this._shadowRootBody = null, this._width = this._height = 0, this._resizeHandlers = [];
        }), t.after("styleSelf", function () {
            return this._updateCachedDimensions();
        });
    });
}, function (t, e) {
    var n;
    (n = function (t, e) {
        function r(t, e) {
            if (t.resizedAttached) {
                if (t.resizedAttached) return void t.resizedAttached.add(e);
            } else t.resizedAttached = new function () {
                var t,
                    e;
                this.q = [], this.add = function (t) {
                    this.q.push(t);
                }, this.call = function () {
                    for (t = 0, e = this.q.length; t < e; t++) this.q[t].call();
                };
            }, t.resizedAttached.add(e);
            t.resizeSensor = document.createElement("div"), t.resizeSensor.className = "resize-sensor";
            var n = "position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: hidden; z-index: -1; visibility: hidden;",
                r = "position: absolute; left: 0; top: 0; transition: 0s;";
            t.resizeSensor.style.cssText = n, t.resizeSensor.innerHTML = '<div class="resize-sensor-expand" style="' + n + '"><div style="' + r + '"></div></div><div class="resize-sensor-shrink" style="' + n + '"><div style="' + r + ' width: 200%; height: 200%"></div></div>', t.appendChild(t.resizeSensor), { fixed: 1, absolute: 1 }[function (t, e) {
                return t.currentStyle ? t.currentStyle[e] : window.getComputedStyle ? window.getComputedStyle(t, null).getPropertyValue(e) : t.style[e];
            }(t, "position")] || (t.style.position = "relative");
            var i,
                o,
                s = t.resizeSensor.childNodes[0],
                a = s.childNodes[0],
                u = t.resizeSensor.childNodes[1],
                c = (u.childNodes[0], function () {
                    a.style.width = s.offsetWidth + 10 + "px", a.style.height = s.offsetHeight + 10 + "px", s.scrollLeft = s.scrollWidth, s.scrollTop = s.scrollHeight, u.scrollLeft = u.scrollWidth, u.scrollTop = u.scrollHeight, i = t.offsetWidth, o = t.offsetHeight;
                });
            c();
            var d = function (t, e, n) {
                    t.attachEvent ? t.attachEvent("on" + e, n) : t.addEventListener(e, n);
                },
                f = function () {
                    t.offsetWidth == i && t.offsetHeight == o || t.resizedAttached && t.resizedAttached.call(), c();
                };
            d(s, "scroll", f), d(u, "scroll", f);
        }

        var i = Object.prototype.toString.call(t),
            o = "[object Array]" === i || "[object NodeList]" === i || "[object HTMLCollection]" === i || "undefined" != typeof jQuery && t instanceof jQuery || "undefined" != typeof Elements && t instanceof Elements;
        if (o) for (var s = 0, a = t.length; s < a; s++) r(t[s], e); else r(t, e);
        this.detach = function () {
            if (o) for (var e = 0, r = t.length; e < r; e++) n.detach(t[e]); else n.detach(t);
        };
    }).detach = function (t) {
        t.resizeSensor && (t.removeChild(t.resizeSensor), delete t.resizeSensor, delete t.resizedAttached);
    }, void 0 !== t && void 0 !== t.exports ? t.exports = n : window.ResizeSensor = n;
}, function (t, e, n) {
    var r = n(2),
        i = n(0),
        o = n(11),
        s = n(12)(),
        a = n(62),
        u = n(5),
        c = "a.twitter-follow-button";
    t.exports = function (t) {
        return s(t, c).map(function (t) {
            return a(function (t) {
                var e = o(t),
                    n = { screenName: r.screenName(t.href), showScreenName: "false" !== t.getAttribute("data-show-screen-name"), showCount: "false" !== t.getAttribute("data-show-count"), size: t.getAttribute("data-size"), count: t.getAttribute("data-count"), preview: t.getAttribute("data-preview") };
                return i.forIn(n, function (t, n) {
                    var r = e[t];
                    e[t] = u.hasValue(r) ? r : n;
                }), e.screenName = e.screenName || e.screen_name, e;
            }(t), t.parentNode, t);
        });
    };
}, function (t, e, n) {
    var r = n(3);
    t.exports = function (t, e) {
        var i = new r;
        return n.e(2).then(function (r) {
            var o;
            try {
                o = n(76), i.resolve(new o(t, e));
            } catch (t) {
                i.reject(t);
            }
        }.bind(null, n)).catch(function (t) {
            i.reject(t);
        }), i.promise;
    };
}, function (t, e, n) {
    var r = n(17),
        i = n(117),
        o = n(59),
        s = n(28),
        a = n(3),
        u = n(6),
        c = n(7),
        d = n(0),
        f = { allowfullscreen: "true" },
        l = { position: "absolute", visibility: "hidden", width: "0px", height: "0px" },
        h = { position: "static", visibility: "visible" },
        p = {};
    i(function (t, e, n) {
        var r = p[t];
        if (r) return e = e || 1, n = n || 1, r.styleSelf({ width: e + "px", height: n + "px" }).then(function () {
            r.didResize();
        });
    }, function (t) {
        var e = p[t];
        if (e) return e._results.resolve();
    }, function (t) {
        var e = p[t];
        if (e) return e._rendered.resolve();
    }, function (t, e) {
        var n = p[t];
        n && e && n.setIframeVersion(e);
    }, function (t) {
        var e = p[t];
        if (e) return e._results.reject();
    }), t.exports = function (t) {
        t.overrideProperty("id", {
            get: function () {
                return this.sandboxEl && this.sandboxEl.id;
            }
        }), t.overrideProperty("initialized", {
            get: function () {
                return !!this.iframeEl;
            }
        }), t.overrideProperty("width", {
            get: function () {
                return this._width;
            }
        }), t.overrideProperty("height", {
            get: function () {
                return this._height;
            }
        }), t.overrideProperty("sandboxEl", {
            get: function () {
                return this.iframeEl;
            }
        }), t.defineProperty("iframeEl", {
            get: function () {
                return this._iframe;
            }
        }), t.defineProperty("iframeVersion", {
            get: function () {
                return this._iframeVersion;
            }
        }), t.define("updateCachedDimensions", function () {
            var t = this;
            return this.initialized ? r.read(function () {
                t._width = t.sandboxEl.offsetWidth, t._height = t.sandboxEl.offsetHeight;
            }) : u.resolve();
        }), t.define("setTitle", function (t) {
            this.iframeEl.title = t;
        }), t.define("setWaitToSwapUntilRendered", function (t) {
            this._waitToSwapUntilRendered = t;
        }), t.define("setIframeVersion", function (t) {
            this._iframeVersion = t;
        }), t.define("getResultsPromise", function () {
            return this._results.promise;
        }), t.define("getRenderedPromise", function () {
            return this._rendered.promise;
        }), t.define("makeVisible", function () {
            return this.styleSelf(h);
        }), t.define("didResize", function () {
            var t = this,
                e = t._resizeHandlers.length > 0;
            return this.updateCachedDimensions().then(function () {
                e && t._resizeHandlers.forEach(function (e) {
                    e(t);
                });
            });
        }), t.define("loadDocument", function (t) {
            var e = new a;
            return this.initialized ? this.iframeEl.src ? u.reject(new Error("widget already loaded")) : (this.iframeEl.addEventListener("load", e.resolve, !1), this.iframeEl.addEventListener("error", e.reject, !1), this.iframeEl.src = t, e.promise) : u.reject(new Error("sandbox not initialized"));
        }), t.after("initialize", function () {
            var t = new a,
                e = new a;
            this._iframe = null, this._iframeVersion = null, this._width = this._height = 0, this._resizeHandlers = [], this._rendered = t, this._results = e, this._waitToSwapUntilRendered = !1;
        }), t.override("insert", function (t, e, n, i) {
            var a = this;
            return e = d.aug({ id: t }, f, e), n = d.aug({}, l, n), this._iframe = s(e, n), p[t] = this, a._waitToSwapUntilRendered || this.onResize(o(function () {
                a.makeVisible();
            })), r.write(c(i, null, this._iframe));
        }), t.override("onResize", function (t) {
            this._resizeHandlers.push(t);
        }), t.after("styleSelf", function () {
            return this.updateCachedDimensions();
        });
    };
}, function (t, e, n) {
    var r = n(1),
        i = n(118),
        o = n(120),
        s = n(22),
        a = n(5),
        u = n(121);
    t.exports = function (t, e, n, c, d) {
        function f(t) {
            var e = u(this);
            s.trigger(t.type, { target: e, region: t.region, type: t.type, data: t.data || {} });
        }

        function l(e) {
            var n = u(this),
                r = n && n.id,
                i = a.asInt(e.width),
                o = a.asInt(e.height);
            r && void 0 !== i && void 0 !== o && t(r, i, o);
        }

        (new i).attachReceiver(new o.Receiver(r, "twttr.button")).bind("twttr.private.trigger", f).bind("twttr.private.resizeButton", l), (new i).attachReceiver(new o.Receiver(r, "twttr.embed")).bind("twttr.private.initialized", function (t) {
            var e = u(this),
                n = e && e.id,
                r = t.iframe_version;
            n && r && c && c(n, r);
        }).bind("twttr.private.trigger", f).bind("twttr.private.results", function () {
            var t = u(this),
                n = t && t.id;
            n && e && e(n);
        }).bind("twttr.private.rendered", function () {
            var t = u(this),
                e = t && t.id;
            e && n && n(e);
        }).bind("twttr.private.no_results", function () {
            var t = u(this),
                e = t && t.id;
            e && d && d(e);
        }).bind("twttr.private.resize", l);
    };
}, function (t, e, n) {
    var r = n(24),
        i = n(119),
        o = n(0),
        s = n(6),
        a = n(23),
        u = "2.0";

    function c(t) {
        this.registry = t || {};
    }

    function d(t) {
        var e,
            n;
        return e = o.isType("string", t), n = o.isType("number", t), e || n || null === t;
    }

    function f(t, e) {
        return { jsonrpc: u, id: d(t) ? t : null, error: e };
    }

    c.prototype._invoke = function (t, e) {
        var n,
            r,
            i;
        n = this.registry[t.method], r = t.params || [], r = o.isType("array", r) ? r : [r];
        try {
            i = n.apply(e.source || null, r);
        } catch (t) {
            i = s.reject(t.message);
        }
        return a.isPromise(i) ? i : s.resolve(i);
    }, c.prototype._processRequest = function (t, e) {
        var n,
            r;
        return function (t) {
            var e,
                n,
                r;
            return !!o.isObject(t) && (e = t.jsonrpc === u, n = o.isType("string", t.method), r = !("id" in t) || d(t.id), e && n && r);
        }(t) ? (n = "params" in t && (r = t.params, !o.isObject(r) || o.isType("function", r)) ? s.resolve(f(t.id, i.INVALID_PARAMS)) : this.registry[t.method] ? this._invoke(t, { source: e }).then(function (e) {
            return n = t.id, { jsonrpc: u, id: n, result: e };
            var n;
        }, function () {
            return f(t.id, i.INTERNAL_ERROR);
        }) : s.resolve(f(t.id, i.METHOD_NOT_FOUND)), null != t.id ? n : s.resolve()) : s.resolve(f(t.id, i.INVALID_REQUEST));
    }, c.prototype.attachReceiver = function (t) {
        return t.attachTo(this), this;
    }, c.prototype.bind = function (t, e) {
        return this.registry[t] = e, this;
    }, c.prototype.receive = function (t, e) {
        var n,
            a,
            u,
            c = this;
        try {
            u = t, t = o.isType("string", u) ? r.parse(u) : u;
        } catch (t) {
            return s.resolve(f(null, i.PARSE_ERROR));
        }
        return e = e || null, a = ((n = o.isType("array", t)) ? t : [t]).map(function (t) {
            return c._processRequest(t, e);
        }), n ? function (t) {
            return s.all(t).then(function (t) {
                return (t = t.filter(function (t) {
                    return void 0 !== t;
                })).length ? t : void 0;
            });
        }(a) : a[0];
    }, t.exports = c;
}, function (t) {
    t.exports = { PARSE_ERROR: { code: -32700, message: "Parse error" }, INVALID_REQUEST: { code: -32600, message: "Invalid Request" }, INVALID_PARAMS: { code: -32602, message: "Invalid params" }, METHOD_NOT_FOUND: { code: -32601, message: "Method not found" }, INTERNAL_ERROR: { code: -32603, message: "Internal error" } };
}, function (t, e, n) {
    var r = n(8),
        i = n(1),
        o = n(24),
        s = n(3),
        a = n(20),
        u = n(0),
        c = n(2),
        d = n(7),
        f = a.ie9();

    function l(t, e, n) {
        var r;
        t && t.postMessage && (f ? r = (n || "") + o.stringify(e) : n ? (r = {})[n] = e : r = e, t.postMessage(r, "*"));
    }

    function h(t) {
        return u.isType("string", t) ? t : "JSONRPC";
    }

    function p(t, e) {
        return e ? u.isType("string", t) && 0 === t.indexOf(e) ? t.substring(e.length) : t && t[e] ? t[e] : void 0 : t;
    }

    function m(t, e) {
        var n = t.document;
        this.filter = h(e), this.server = null, this.isTwitterFrame = c.isTwitterURL(n.location.href), t.addEventListener("message", d(this._onMessage, this), !1);
    }

    function v(t, e) {
        this.pending = {}, this.target = t, this.isTwitterHost = c.isTwitterURL(r.href), this.filter = h(e), i.addEventListener("message", d(this._onMessage, this), !1);
    }

    u.aug(m.prototype, {
        _onMessage: function (t) {
            var e,
                n = this;
            this.server && (this.isTwitterFrame && !c.isTwitterURL(t.origin) || (e = p(t.data, this.filter)) && this.server.receive(e, t.source).then(function (e) {
                e && l(t.source, e, n.filter);
            }));
        }, attachTo: function (t) {
            this.server = t;
        }, detach: function () {
            this.server = null;
        }
    }), u.aug(v.prototype, {
        _processResponse: function (t) {
            var e = this.pending[t.id];
            e && (e.resolve(t), delete this.pending[t.id]);
        }, _onMessage: function (t) {
            var e;
            if ((!this.isTwitterHost || c.isTwitterURL(t.origin)) && (e = p(t.data, this.filter))) {
                if (u.isType("string", e)) try {
                    e = o.parse(e);
                } catch (t) {
                    return;
                }
                (e = u.isType("array", e) ? e : [e]).forEach(d(this._processResponse, this));
            }
        }, send: function (t) {
            var e = new s;
            return t.id ? this.pending[t.id] = e : e.resolve(), l(this.target, t, this.filter), e.promise;
        }
    }), t.exports = {
        Receiver: m, Dispatcher: v, _stringifyPayload: function (t) {
            return arguments.length > 0 && (f = !!t), f;
        }
    };
}, function (t, e, n) {
    var r = n(4);
    t.exports = function (t) {
        for (var e, n = r.getElementsByTagName("iframe"), i = 0; n[i]; i++) if ((e = n[i]).contentWindow === t) return e;
    };
}, function (t, e, n) {
    var r = n(5),
        i = n(0),
        o = n(2),
        s = n(11),
        a = n(12)(),
        u = n(63),
        c = "a.twitter-moment";
    t.exports = function (t) {
        return a(t, c).map(function (t) {
            return u(function (t) {
                var e = s(t),
                    n = { momentId: o.momentId(t.href), chrome: t.getAttribute("data-chrome"), limit: t.getAttribute("data-limit") };
                return i.forIn(n, function (t, n) {
                    var i = e[t];
                    e[t] = r.hasValue(i) ? i : n;
                }), e;
            }(t), t.parentNode, t);
        });
    };
}, function (t, e, n) {
    var r = n(3);
    t.exports = function (t, e) {
        var i = new r;
        return n.e(3).then(function (r) {
            var o;
            try {
                o = n(77), i.resolve(new o(t, e));
            } catch (t) {
                i.reject(t);
            }
        }.bind(null, n)).catch(function (t) {
            i.reject(t);
        }), i.promise;
    };
}, function (t, e, n) {
    var r = n(0),
        i = n(11),
        o = n(12)(),
        s = n(64),
        a = "a.periscope-on-air",
        u = /^https?:\/\/(?:www\.)?(?:periscope|pscp)\.tv\/@?([a-zA-Z0-9_]+)\/?$/i;
    t.exports = function (t) {
        return o(t, a).map(function (t) {
            return s(function (t) {
                var e = i(t),
                    n = t.getAttribute("href"),
                    o = t.getAttribute("data-size"),
                    s = u.exec(n)[1];
                return r.aug(e, { username: s, size: o });
            }(t), t.parentNode, t);
        });
    };
}, function (t, e, n) {
    var r = n(3);
    t.exports = function (t, e) {
        var i = new r;
        return n.e(4).then(function (r) {
            var o;
            try {
                o = n(78), i.resolve(new o(t, e));
            } catch (t) {
                i.reject(t);
            }
        }.bind(null, n)).catch(function (t) {
            i.reject(t);
        }), i.promise;
    };
}, function (t, e, n) {
    var r = n(5),
        i = n(0),
        o = n(65),
        s = n(11),
        a = n(12)(),
        u = n(66),
        c = n(2),
        d = n(18),
        f = "a.twitter-timeline,div.twitter-timeline,a.twitter-grid",
        l = "Embedded Search timelines have been deprecated. See https://twittercommunity.com/t/deprecating-widget-settings/102295.",
        h = "You may have been affected by an update to settings in embedded timelines. See https://twittercommunity.com/t/deprecating-widget-settings/102295.",
        p = "Embedded grids have been deprecated and will now render as timelines. Please update your embed code to use the twitter-timeline class. More info: https://twittercommunity.com/t/update-on-the-embedded-grid-display-type/119564.";
    t.exports = function (t, e) {
        return a(t, f).map(function (t) {
            return u(function (t) {
                var e = s(t),
                    n = t.getAttribute("data-show-replies"),
                    a = { isPreconfigured: !!t.getAttribute("data-widget-id"), chrome: t.getAttribute("data-chrome"), tweetLimit: t.getAttribute("data-tweet-limit") || t.getAttribute("data-limit"), ariaLive: t.getAttribute("data-aria-polite"), theme: t.getAttribute("data-theme"), borderColor: t.getAttribute("data-border-color"), showReplies: n ? r.asBoolean(n) : null, profileScreenName: t.getAttribute("data-screen-name"), profileUserId: t.getAttribute("data-user-id"), favoritesScreenName: t.getAttribute("data-favorites-screen-name"), favoritesUserId: t.getAttribute("data-favorites-user-id"), likesScreenName: t.getAttribute("data-likes-screen-name"), likesUserId: t.getAttribute("data-likes-user-id"), listOwnerScreenName: t.getAttribute("data-list-owner-screen-name"), listOwnerUserId: t.getAttribute("data-list-owner-id"), listId: t.getAttribute("data-list-id"), listSlug: t.getAttribute("data-list-slug"), customTimelineId: t.getAttribute("data-custom-timeline-id"), staticContent: t.getAttribute("data-static-content"), url: t.href };
                return a.isPreconfigured && (c.isSearchUrl(a.url) ? d.publicError(l, t) : d.publicLog(h, t)), "twitter-grid" === t.className && d.publicLog(p, t), (a = i.aug(a, e)).dataSource = o(a), a.id = a.dataSource && a.dataSource.id, a;
            }(t), t.parentNode, t, e);
        });
    };
}, function (t, e) {
    t.exports = function (t, e, n) {
        for (var r, i = 0; i < t.length; i++) if (r = e.call(n, t[i], i, t)) return r;
    };
}, function (t, e, n) {
    var r = n(31);
    t.exports = r.build([n(32), n(131)]);
}, function (t, e, n) {
    var r = n(0),
        i = n(130);
    t.exports = function (t) {
        return "en" === t || r.contains(i, t);
    };
}, function (t, e) {
    t.exports = ["hi", "zh-cn", "fr", "zh-tw", "msa", "fil", "fi", "sv", "pl", "ja", "ko", "de", "it", "pt", "es", "ru", "id", "tr", "da", "no", "nl", "hu", "fa", "ar", "ur", "he", "th", "cs", "uk", "vi", "ro", "bn", "el", "en-gb", "gu", "kn", "mr", "ta", "bg", "ca", "hr", "sr", "sk"];
}, function (t, e, n) {
    var r = n(2),
        i = n(44),
        o = "collection:";

    function s(t, e) {
        return r.collectionId(t) || e;
    }

    t.exports = function (t) {
        t.params({ id: {}, url: {} }), t.overrideProperty("id", {
            get: function () {
                var t = s(this.params.url, this.params.id);
                return o + t;
            }
        }), t.before("initialize", function () {
            if (!s(this.params.url, this.params.id)) throw new Error("one of url or id is required");
            i();
        });
    };
}, function (t, e, n) {
    var r = n(31);
    t.exports = r.build([n(32), n(133)]);
}, function (t, e, n) {
    var r = n(2),
        i = n(44),
        o = "likes:";

    function s(t) {
        return r.likesScreenName(t.url) || t.screenName;
    }

    t.exports = function (t) {
        t.params({ screenName: {}, userId: {}, url: {} }), t.overrideProperty("id", {
            get: function () {
                var t = s(this.params) || this.params.userId;
                return o + t;
            }
        }), t.before("initialize", function () {
            if (!s(this.params) && !this.params.userId) throw new Error("screen name or user id is required");
            i();
        });
    };
}, function (t, e, n) {
    var r = n(31);
    t.exports = r.build([n(32), n(135)]);
}, function (t, e, n) {
    var r = n(2),
        i = n(0),
        o = n(27),
        s = "list:";

    function a(t) {
        var e = r.listScreenNameAndSlug(t.url) || t;
        return i.compact({ screen_name: e.ownerScreenName, user_id: e.ownerUserId, list_slug: e.slug });
    }

    t.exports = function (t) {
        t.params({ id: {}, ownerScreenName: {}, ownerUserId: {}, slug: {}, url: {} }), t.overrideProperty("id", {
            get: function () {
                var t,
                    e,
                    n;
                return this.params.id ? s + this.params.id : (e = (t = a(this.params)) && t.list_slug.replace(/-/g, "_"), n = t && (t.screen_name || t.user_id), s + (n + ":") + e);
            }
        }), t.overrideProperty("endpoint", {
            get: function () {
                var t,
                    e = ["timeline-list"];
                return this.params.id ? e.push("list-id", this.params.id) : (t = a(this.params), e.push("screen-name", t.screen_name, "slug", t.list_slug)), o.embedService(e);
            }
        }), t.before("initialize", function () {
            var t = a(this.params);
            if (i.isEmptyObject(t) && !this.params.id) throw new Error("qualified slug or list id required");
        });
    };
}, function (t, e, n) {
    var r = n(31);
    t.exports = r.build([n(32), n(137)]);
}, function (t, e, n) {
    var r = n(2),
        i = n(5),
        o = n(0),
        s = n(27),
        a = "profile:";

    function u(t, e) {
        return r.screenName(t) || e;
    }

    t.exports = function (t) {
        t.params({ showReplies: { fallback: !1, transform: i.asBoolean }, screenName: {}, userId: {}, url: {} }), t.overrideProperty("id", {
            get: function () {
                var t = u(this.params.url, this.params.screenName);
                return a + (t || this.params.userId);
            }
        }), t.overrideProperty("endpoint", {
            get: function () {
                var t = ["timeline-profile"],
                    e = u(this.params.url, this.params.screenName);
                return e ? t.push("screen-name", e) : t.push("user-id", this.params.userId), s.embedService(t);
            }
        }), t.around("queryParams", function (t) {
            return o.aug(t(), { showReplies: this.params.showReplies ? "true" : "false" });
        }), t.before("initialize", function () {
            if (!u(this.params.url, this.params.screenName) && !this.params.userId) throw new Error("screen name or user id is required");
        });
    };
}, function (t, e, n) {
    var r = n(3);
    t.exports = function (t, e) {
        var i = new r;
        return n.e(5).then(function (r) {
            var o;
            try {
                o = n(79), i.resolve(new o(t, e));
            } catch (t) {
                i.reject(t);
            }
        }.bind(null, n)).catch(function (t) {
            i.reject(t);
        }), i.promise;
    };
}, function (t, e, n) {
    var r = n(10),
        i = n(2),
        o = n(0),
        s = n(11),
        a = n(12)(),
        u = n(67),
        c = "blockquote.twitter-tweet, blockquote.twitter-video",
        d = /\btw-align-(left|right|center)\b/;
    t.exports = function (t, e) {
        return a(t, c).map(function (t) {
            return u(function (t) {
                var e = s(t),
                    n = t.getElementsByTagName("A"),
                    a = n && n[n.length - 1],
                    u = a && i.status(a.href),
                    c = t.getAttribute("data-conversation"),
                    f = "none" == c || "hidden" == c || r.present(t, "tw-hide-thread"),
                    l = t.getAttribute("data-cards"),
                    h = "none" == l || "hidden" == l || r.present(t, "tw-hide-media"),
                    p = t.getAttribute("data-media-max-width"),
                    m = t.getAttribute("data-align") || t.getAttribute("align"),
                    v = t.getAttribute("data-theme");
                return !m && d.test(t.className) && (m = RegExp.$1), o.aug(e, { tweetId: u, hideThread: f, hideCard: h, mediaMaxWidth: p, align: m, theme: v, id: u });
            }(t), t.parentNode, t, e);
        });
    };
}, function (t, e, n) {
    var r = n(3);
    t.exports = function (t, e) {
        var i = new r;
        return n.e(6).then(function (r) {
            var o;
            try {
                o = n(80), i.resolve(new o(t, e));
            } catch (t) {
                i.reject(t);
            }
        }.bind(null, n)).catch(function (t) {
            i.reject(t);
        }), i.promise;
    };
}, function (t, e, n) {
    var r = n(10),
        i = n(0),
        o = n(11),
        s = n(12)(),
        a = n(68),
        u = n(5),
        c = "a.twitter-share-button, a.twitter-mention-button, a.twitter-hashtag-button",
        d = "twitter-hashtag-button",
        f = "twitter-mention-button";
    t.exports = function (t) {
        return s(t, c).map(function (t) {
            return a(function (t) {
                var e = o(t),
                    n = { screenName: t.getAttribute("data-button-screen-name"), text: t.getAttribute("data-text"), type: t.getAttribute("data-type"), size: t.getAttribute("data-size"), url: t.getAttribute("data-url"), hashtags: t.getAttribute("data-hashtags"), via: t.getAttribute("data-via"), buttonHashtag: t.getAttribute("data-button-hashtag") };
                return i.forIn(n, function (t, n) {
                    var r = e[t];
                    e[t] = u.hasValue(r) ? r : n;
                }), e.screenName = e.screenName || e.screen_name, e.buttonHashtag = e.buttonHashtag || e.button_hashtag || e.hashtag, r.present(t, d) && (e.type = "hashtag"), r.present(t, f) && (e.type = "mention"), e;
            }(t), t.parentNode, t);
        });
    };
}, function (t, e, n) {
    var r = n(3);
    t.exports = function (t, e) {
        var i = new r;
        return n.e(2).then(function (r) {
            var o;
            try {
                o = n(81), i.resolve(new o(t, e));
            } catch (t) {
                i.reject(t);
            }
        }.bind(null, n)).catch(function (t) {
            i.reject(t);
        }), i.promise;
    };
}, function (t, e, n) {
    var r = n(0);
    t.exports = r.aug({}, n(144), n(145), n(146), n(147), n(148), n(149), n(150));
}, function (t, e, n) {
    var r = n(60),
        i = n(15)(["userId"], {}, r);
    t.exports = { createDMButton: i };
}, function (t, e, n) {
    var r = n(62),
        i = n(15)(["screenName"], {}, r);
    t.exports = { createFollowButton: i };
}, function (t, e, n) {
    var r = n(63),
        i = n(15)(["momentId"], {}, r);
    t.exports = { createMoment: i };
}, function (t, e, n) {
    var r = n(64),
        i = n(15)(["username"], {}, r);
    t.exports = { createPeriscopeOnAirButton: i };
}, function (t, e, n) {
    var r = n(8),
        i = n(18),
        o = n(2),
        s = n(0),
        a = n(5),
        u = n(65),
        c = n(66),
        d = n(15)([], {}, c),
        f = n(6),
        l = "Embedded grids have been deprecated. Please use twttr.widgets.createTimeline instead. More info: https://twittercommunity.com/t/update-on-the-embedded-grid-display-type/119564.",
        h = {
            createTimeline: p, createGridFromCollection: function (t) {
                var e = s.toRealArray(arguments).slice(1),
                    n = { sourceType: "collection", id: t };
                return e.unshift(n), i.publicLog(l), p.apply(this, e);
            }
        };

    function p(t) {
        var e,
            n = s.toRealArray(arguments).slice(1);
        return a.isString(t) || a.isNumber(t) ? f.reject("Embedded timelines with widget settings have been deprecated. See https://twittercommunity.com/t/deprecating-widget-settings/102295.") : s.isObject(t) ? (t = t || {}, n.forEach(function (t) {
            s.isType("object", t) && function (t) {
                t.ariaLive = t.ariaPolite;
            }(e = t);
        }), e || (e = {}, n.push(e)), t.lang = e.lang, t.tweetLimit = e.tweetLimit, t.showReplies = e.showReplies, e.dataSource = u(t), d.apply(this, n)) : f.reject("data source must be an object.");
    }

    o.isTwitterURL(r.href) && (h.createTimelinePreview = function (t, e, n) {
        var r = { previewParams: t, useLegacyDefaults: !0, isPreviewTimeline: !0 };
        return r.dataSource = u(r), d(e, r, n);
    }), t.exports = h;
}, function (t, e, n) {
    var r,
        i = n(0),
        o = n(67),
        s = n(15),
        a = (r = s(["tweetId"], {}, o), function () {
            return i.toRealArray(arguments).slice(1).forEach(function (t) {
                i.isType("object", t) && (t.hideCard = "none" == t.cards || "hidden" == t.cards, t.hideThread = "none" == t.conversation || "hidden" == t.conversation);
            }), r.apply(this, arguments);
        });
    t.exports = { createTweet: a, createTweetEmbed: a, createVideo: a };
}, function (t, e, n) {
    var r = n(0),
        i = n(68),
        o = n(15),
        s = o(["url"], { type: "share" }, i),
        a = o(["buttonHashtag"], { type: "hashtag" }, i),
        u = o(["screenName"], { type: "mention" }, i);

    function c(t) {
        return function () {
            return r.toRealArray(arguments).slice(1).forEach(function (t) {
                r.isType("object", t) && (t.screenName = t.screenName || t.screen_name, t.buttonHashtag = t.buttonHashtag || t.button_hashtag || t.hashtag);
            }), t.apply(this, arguments);
        };
    }

    t.exports = { createShareButton: c(s), createHashtagButton: c(a), createMentionButton: c(u) };
}, function (t, e, n) {
    var r,
        i,
        o,
        s = n(4),
        a = n(1),
        u = 0,
        c = [],
        d = s.createElement("a");

    function f() {
        var t,
            e;
        for (u = 1, t = 0, e = c.length; t < e; t++) c[t]();
    }

    /^loade|c/.test(s.readyState) && (u = 1), s.addEventListener && s.addEventListener("DOMContentLoaded", i = function () {
        s.removeEventListener("DOMContentLoaded", i, !1), f();
    }, !1), d.doScroll && s.attachEvent("onreadystatechange", r = function () {
        /^c/.test(s.readyState) && (s.detachEvent("onreadystatechange", r), f());
    }), o = d.doScroll ? function (t) {
        a.self != a.top ? u ? t() : c.push(t) : function () {
            try {
                d.doScroll("left");
            } catch (e) {
                return setTimeout(function () {
                    o(t);
                }, 50);
            }
            t();
        }();
    } : function (t) {
        u ? t() : c.push(t);
    }, t.exports = o;
}, function (t, e, n) {
    var r = n(42),
        i = n(9);
    t.exports = function () {
        i.set("buildVersion", r.version);
    };
}, function (t, e, n) {
    n(154), n(157), n(158);
}, function (t, e, n) {
    var r = n(155),
        i = n(26),
        o = n(69),
        s = new r,
        a = function (t) {
            t.widgets && 1 === t.widgets.length && (s.start(), i.emitter.unbind(i.ALL_WIDGETS_RENDER_START, a));
        },
        u = function (t) {
            var e;
            t.widgets && 1 === t.widgets.length && (e = t.widgets[0], s.end(), e.dataset && e.dataset.tweetId && o({ duration: s.duration(), namespace: { element: "tweet", action: "render" }, widgetIds: [e.dataset.tweetId] })), i.emitter.unbind(i.ALL_WIDGETS_RENDER_END, u);
        };
    i.emitter.bind(i.ALL_WIDGETS_RENDER_START, a), i.emitter.bind(i.ALL_WIDGETS_RENDER_END, u);
}, function (t, e, n) {
    var r = n(156);

    function i() {
    }

    i.prototype.start = function () {
        this._startTime = r();
    }, i.prototype.end = function () {
        this._duration = r() - this._startTime;
    }, i.prototype.duration = function () {
        return this._duration;
    }, t.exports = i;
}, function (t, e, n) {
    var r = n(1);
    t.exports = function () {
        return r.performance && r.performance.now ? r.performance.now() : Date.now();
    };
}, function (t, e, n) {
    var r,
        i = n(26),
        o = 0;

    function s() {
        r && r.length === o && (i.emitter.trigger(i.ALL_WIDGETS_AND_IMAGES_LOADED, r), r = null);
    }

    i.emitter.bind(i.ALL_WIDGETS_RENDER_END, function (t) {
        r = t.widgets, s();
    }), t.exports = {
        reportImagesLoadForAWidget: function () {
            o++, s();
        }
    };
}, function (t, e, n) {
    var r = n(26),
        i = n(69),
        o = n(159),
        s = n(2),
        a = n(1),
        u = n(0),
        c = n(20),
        d = n(61);

    function f(t) {
        return t.performance.getEntriesByType("resource").filter(function (t) {
            return s.isTwimgURL(t.name) || s.isTwitterURL(t.name);
        }).reduce(function (t, e) {
            return t[e.name] = e.duration, t;
        }, {});
    }

    r.emitter.bind(r.ALL_WIDGETS_AND_IMAGES_LOADED, function (t) {
        var e,
            n,
            r = [];
        c.hasPerformanceInformation() && (e = f(a), d.isSupported() || (r = function (t) {
            return t.reduce(function (t, e) {
                return u.aug(t, f(e.contentDocument.defaultView));
            }, {});
        }(t)), n = u.aug({}, e, r), Object.keys(o).forEach(function (t) {
            !function (t, e, n) {
                var r = Object.keys(t).reduce(function (e, r) {
                    return n(r) ? e + t[r] : e;
                }, 0);
                i({ duration: r, namespace: { element: e, action: "resource" } });
            }(n, t, o[t]);
        }));
    });
}, function (t, e, n) {
    var r = n(2),
        i = {
            all: function () {
                return !0;
            }, image: function (t) {
                return r.isTwimgURL(t);
            }, settings: function (t) {
                return r.isSettingsURL(t);
            }, widget_iframe: function (t) {
                return r.isWidgetIframeURL(t);
            }
        };
    t.exports = i;
}
])));
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({id: 369, from: "common"}), width: 105, click: function () { // "369":"Отмена",
                        __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({id: 378, from: "common"}), maxWidth: 130, click: _acceptFilesReceive}, // "378":"Принять",
                        { width: 10},
                        { view: "button", value: mcLang({id: 369, from: "common"}),  maxWidth: 130, click: __abortTransfer, type: "danger"}, // "369":"Отмена",
                        {}
                    ]}
                ]}
        }
    };

    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", [($rootScope.chatAliases || mcConst.dataModels).ReceiveFiles]);
                    $rootScope.$broadcast(window._messages_.receiveFiles.setReceiveFilesInfo, [vDirectReceive.getInfo()]);
                }
            }
        ]);
    };

    this._hideProgressInfo = function (allOk){
        if (currentDuState && !allOk && window._messages_.chatFrame){
            $rootScope.$broadcast(window._messages_.chatFrame.scrollDownChat);
        }

        if (allOk){
            switch (currentDuState){
                case downloadConstants.DOWNLOAD:
                case downloadConstants.DOWNLOADFTP:
                    webix.message(mcLang({id: 379, from: "common"})); // "379":"Файл скачан",
                break;

                case  downloadConstants.UPLOAD:
                    webix.message(mcLang({od: 380, from: "common"})); // "380":"Файл загружен на сервер",
                break;
            }
        }

        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({id: 381, from: "common"}) // "381":"Ожидаем согласие на прием файлов...",
                });

                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, [$rootScope.chatAliases.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, [$rootScope.chatAliases.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
        $rootScope.$broadcast(window._messages_.chatFrame.focusToEnterChat);
    }
    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() && (
            // todo: переробити контроль прав вставки файлів із буфера, винести їх окремо в те місце, де воно визивається
            ($rootScope.GetChatType() === $rootScope.chatAliases.UID    && (mcService.checkRights(mcConst._CMD_.RS.QTxtConfSendFiles) || mcService.checkRights(mcConst._CMD_.RS.QSendImages))) ||
            ($rootScope.GetChatType() === $rootScope.chatAliases.UIN    && (mcService.checkRights(mcConst._CMD_.RS.QPrivInsertFiles)  || mcService.checkRights(mcConst._CMD_.RS.QImagesPrivates))) ||
            ($rootScope.GetChatType() === $rootScope.chatAliases.Kanban &&  mcService.checkRights(mcConst._CMD_.RS.QKanbanTaskEdit)) ||
            ($rootScope.GetChatType() === $rootScope.chatAliases.Kanban &&  mcService.checkRights(mcConst._CMD_.RS.QKanbanCommentAdd)) ||
            ($rootScope.GetChatType() === $rootScope.chatAliases.SendBroadcast &&  mcService.checkRights(mcConst._CMD_.RS.QAllowImagesInBroadcasts))
        )) {
            var type    = $rootScope.GetChatType();
            var cText   = "";
            var imgInfo = [];

            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, reject){
                                var image   = item.getAsFile();
                                var _ID     = $rootScope.GetChatID();

                                if (image.size < 10000000){
                                    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();
                                        }
                                    };
                                } else {
                                    Self._uploadFile(mcConst._CMD_.msgType.FILE_NEW, true, e.clipboardData);
                                    
                                    reject({
                                        message: "Image size too large, upload as a file"
                                    });
                                }
                            })
                        }
                    }))
                    .then(function (){
                        __doUploadImage.apply(null, imgInfo.concat(cText ? [cText] : []));
                    })
                    .catch(function (err){
                        console.error(err.message);
                    });
                } else
                if (e.clipboardData.files.length >= 1) {
                    Self._uploadFile(mcConst._CMD_.msgType.FILE_NEW, true, e.clipboardData);
                }
            }
        }
    }

    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._clearPasteItemFromClipboard = function () {
        pastView.removeEventListener("paste", __onPastFile);

        pastView = null;
    };

    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(err){
                    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)) {
            var fileInfo = dropFileInfo.items[0].getAsFile();

            fileType = (
                dropFileInfo.items[0].type === "image/png" ||
                dropFileInfo.items[0].type === "image/jpg" ||
                dropFileInfo.items[0].type === "image/jpeg"||
                dropFileInfo.items[0].type === "image/gif"
            ) ? mcConst._CMD_.msgType.IMAGE_NEW : mcConst._CMD_.msgType.FILE_NEW;

            if (dropFileInfo.items.length === 1 &&
                dropFileInfo.items[0].kind === "file" &&
                Self.pastImagePreview &&
                fileType === mcConst._CMD_.msgType.IMAGE_NEW &&
                fileInfo.size < 10000000 )
            {

                var resolution = "";
                var tempImg    = document.createElement("img");

                tempImg.src    = window.URL.createObjectURL(fileInfo);
                tempImg.onload = function () {
                    resolution   = tempImg.width + "x" + tempImg.height;
                    tempImg.src  = "";
                    tempImg      = null;

                    if (Self.pastImagePreview){
                        Self.pastImagePreview(fileInfo, function () {
                            __doUploadImage(currentID, chatType, fileInfo, resolution);
                        });
                    } else {
                        __doUploadImage(currentID, chatType, fileInfo, 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({id: 376, from: "common"})); // "376":"Передавать файлы напрямую можно только пользователям онлайн!",
                } 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({id: 377, from: "common"}) // "377":"Передача файлов прервана",
                    }]);

                    webix.message(mcLang({id: 377, from: "common"})); // "377":"Передача файлов прервана",

                    $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, [($rootScope.chatAliases || 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])))){
                                     var delName = [];

                                     if (mcService.isArray(item)){
                                         item.forEach(function(el){
                                             delName.push(el[btn.delName]);
                                         });
                                     } else {
                                         delName.push(item[btn.delName]);
                                     }

                                     $rootScope.$broadcast('addShowedWindow', [mcService.RandomHash(4), webix.confirm({
                                         type  : "confirm-warning",
                                         text  : mcService.myReplace(btn.title, btn.delName ? delName.join(', ') : 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({id: 367, from: "common"}), // "367":"Сохранить",
                                     cancelTitle    : mcLang({id: 369, from: "common"}), // "369":"Отмена",
                                     delTitle       : mcLang({id: 368, from: "common"})  // "368":"Удалить",
                                 }, 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!');
     }
 })();

/**
 * Created by Gifer on 25.09.2017.
 */

function McTextFinder($rootScope) {
    // mcService — очікується як глобальна залежність (браузерний контекст)
    "use strict";

    var textDiv      = "messageText";
    var openSpan     = '<fndel class="findItem" tabindex="0">';
    var openText     = '__MCFND_OPEN__';
    var closeText    = '__MCFND_CLOSE__';
    var closeSpan    = '</fndel>';
    var currentTextContainer = null;
    var hasResult    = false;
    var currentText  = "";
    var currentFrame = null;
    var currentIdx   = 0;
    var findRes      = [];
    var pos          = 0;

    // Вставляє рядки в позиції рядка str; indices — {позиція: рядок}
    function insertTextAtIndices(str, indices) {
        var result  = '';
        var lastIdx = 0;
        var keys    = Object.keys(indices).map(Number).sort(function(a, b) { return a - b; });

        for (var i = 0; i < keys.length; i++) {
            var idx  = keys[i];
            result  += str.slice(lastIdx, idx) + indices[idx];
            lastIdx  = idx;
        }

        return result + str.slice(lastIdx);
    }

    function getElements(frame, tc) {
        if (!tc) {
            return frame.getElementsByClassName(textDiv);
        }

        var classes = tc.class ? tc.class.split(',').map(function(s) { return s.trim(); }).filter(Boolean) : [];
        var tags    = tc.tag   ? tc.tag.split(',').map(function(s)   { return s.trim().toLowerCase(); }).filter(Boolean) : [];

        if (!classes.length && !tags.length) {
            return [frame];
        }

        var selector;

        if (classes.length && tags.length) {
            var parts = [];
            tags.forEach(function(tag) {
                classes.forEach(function(cls) { parts.push(tag + '.' + cls); });
            });
            selector = parts.join(', ');
        } else if (classes.length) {
            selector = classes.map(function(c) { return '.' + c; }).join(', ');
        } else {
            selector = tags.join(', ');
        }

        return frame.querySelectorAll(selector);
    }

    function focusElem(searchField) {
        if (searchField){
            pos = mcService.getCaretPosition(searchField.getInputNode());

            if (findRes[currentIdx]){
                findRes[currentIdx].focus();
                findRes[currentIdx].className += " findItemSelected";
            }

            searchField.focus();

            mcService.setCaretPosition(searchField.getInputNode(), pos);
        }
    }

    // Повертає об'єкт позицій {pos: marker} для fuzzy-збігу або null
    function getFuzzyPositions(lowerText, searchText) {
        var tokens = [];
        var regex  = /\S+/g;
        var match;

        while ((match = regex.exec(lowerText)) !== null) {
            tokens.push({ val: match[0], start: match.index });
        }

        if (!tokens.length) return null;

        // Передаємо plain-string масив (без keys-об'єктів), щоб уникнути
        // field-length normalization, яка псує скор для коротких слів
        var wordList = tokens.map(function(t) { return t.val; });

        var fuse = new Fuse(wordList, {
            includeScore     : true,
            threshold        : 0.34,
            ignoreLocation   : true,
            minMatchCharLength: 4
        });

        var results = fuse.search(searchText);

        if (!results.length) return null;

        var res = {};
        for (var i = 0; i < results.length; i++) {
            var token = tokens[results[i].refIndex];
            res[token.start]              = openText;
            res[token.start + token.val.length] = closeText;
        }

        return res;
    }

    // Fuzzy-підсвічування одного токена в рядку (обгортка getFuzzyPositions)
    function fuzzyHighlight(originalText, lowerText, searchText) {
        var positions = getFuzzyPositions(lowerText, searchText);
        if (!positions) return null;
        return insertTextAtIndices(originalText, positions);
    }

    // Збирає весь searchable текст з childNodes елемента в один lowercase рядок
    function getFullElementText(items) {
        var parts = [];
        for (var i = 0; i < items.length; i++) {
            var item = items[i];
            if (item.nodeName === '#text') {
                parts.push(item.data.toLowerCase());
            } else if (item.nodeName === 'A' && item.innerText) {
                parts.push(item.innerText.toLowerCase());
            }
        }
        return parts.join(' ');
    }

    // Перевіряє наявність токена в тексті: спочатку exact, потім fuzzy
    function tokenMatchesText(token, lowerText, useFuzzy) {
        if (lowerText.indexOf(token) !== -1) return true;
        if (!useFuzzy || !window.Fuse)       return false;

        var wordList = lowerText.split(/\s+/).filter(Boolean);
        if (!wordList.length) return false;

        var fuse = new Fuse(wordList, {
            threshold        : 0.4,
            ignoreLocation   : true,
            minMatchCharLength: 2
        });

        return fuse.search(token).length > 0;
    }

    // Підсвічує масив токенів у рядку: exact + fuzzy fallback для кожного токена
    function highlightTokensInText(originalText, lowerText, tokens, useFuzzy) {
        var res   = {};
        var found = false;

        for (var t = 0; t < tokens.length; t++) {
            var token = tokens[t];
            var idx   = lowerText.indexOf(token);

            if (idx !== -1) {
                while (idx !== -1) {
                    res[idx]               = openText;
                    res[idx + token.length] = closeText;
                    idx = lowerText.indexOf(token, idx + token.length);
                }
                found = true;
            } else if (useFuzzy && window.Fuse) {
                var positions = getFuzzyPositions(lowerText, token);
                if (positions) {
                    for (var key in positions) {
                        if (positions.hasOwnProperty(key)) {
                            res[key] = positions[key];
                        }
                    }
                    found = true;
                }
            }
        }

        return found ? insertTextAtIndices(originalText, res) : null;
    }

    function findText(frame, text, searchField, textContainer, useFuzzy){
        text = mcService.trim(text.toLowerCase());

        if (text !== "" && text !== currentText){
            if (hasResult) {
                clear(currentFrame);
            }

            pos                  = 0;
            currentFrame         = frame;
            currentTextContainer = (textContainer !== undefined) ? textContainer : null;

            var msgDivList  = getElements(frame, currentTextContainer);
            var queryTokens = text.split(/\s+/).filter(Boolean);
            var multiWord   = queryTokens.length > 1;

            for (var idElm = 0; idElm < msgDivList.length; idElm++) {
                var items        = msgDivList[idElm].childNodes;
                var item;
                var elmHasResult = false;

                if (multiWord) {
                    // Зібрати весь текст елемента для AND/OR перевірки
                    var fullText = getFullElementText(items);

                    // Визначити які токени присутні в елементі (exact або fuzzy)
                    var matchingTokens = queryTokens.filter(function(t) {
                        return tokenMatchesText(t, fullText, useFuzzy);
                    });

                    if (!matchingTokens.length) continue;

                    // AND: всі токени знайшлись → підсвічуємо всі
                    // OR:  частина знайшлась → підсвічуємо тільки знайдені
                    var tokensToHighlight = (matchingTokens.length === queryTokens.length)
                        ? queryTokens
                        : matchingTokens;

                    for (var chdID = 0; chdID < items.length; chdID++) {
                        item = items[chdID];

                        if (item.nodeName === "#text" || item.nodeName === "A") {
                            if (item.nodeName === "A" && item.innerText === "") continue;

                            var byType      = item.nodeName === "A" ? "innerHTML" : "data";
                            var itemText    = item[byType].toLowerCase();
                            var highlighted = highlightTokensInText(item[byType], itemText, tokensToHighlight, useFuzzy);

                            if (highlighted !== null) {
                                item[byType]  = highlighted;
                                elmHasResult  = true;
                                hasResult     = true;
                            }
                        }
                    }
                } else {
                    // Один токен — пошук exact + fuzzy fallback
                    for (var chdID = 0; chdID < items.length; chdID++) {
                        item = items[chdID];

                        if (item.nodeName === "#text" || item.nodeName === "A") {
                            var byType   = item.nodeName === "A" ? "innerHTML" : "data";
                            var itemText = item[byType].toLowerCase();

                            if (itemText.length >= text.length && !(item.nodeName === "A" && item.innerText === "")) {
                                var idx = itemText.indexOf(text);
                                var res = {};

                                if (idx !== -1) {
                                    while (idx !== -1) {
                                        res[idx]               = openText;
                                        res[idx + text.length] = closeText;
                                        idx = itemText.indexOf(text, idx + text.length);
                                    }
                                    item[byType] = insertTextAtIndices(item[byType], res);
                                    elmHasResult = true;
                                    hasResult    = true;
                                } else if (useFuzzy && window.Fuse) {
                                    var highlighted = fuzzyHighlight(item[byType], itemText, text);
                                    if (highlighted !== null) {
                                        item[byType] = highlighted;
                                        elmHasResult = true;
                                        hasResult    = true;
                                    }
                                }
                            }
                        }
                    }
                }

                if (elmHasResult) {
                    msgDivList[idElm].innerHTML =
                        msgDivList[idElm].innerHTML
                            .split(openText).join(openSpan)
                            .split(closeText).join(closeSpan);
                }
            }

            currentText = text;
            findRes     = Array.from(frame.getElementsByClassName("findItem"));
            currentIdx  = 0;

            focusElem(searchField);
        } else

        if (text === currentText && findRes[currentIdx]){
            findRes[currentIdx].className = findRes[currentIdx].className.replace(" findItemSelected", "");

            currentIdx ++;

            if (currentIdx === findRes.length){
                currentIdx = 0;
            }

            focusElem(searchField);
        }
    }

    function clear(frame) {
        if (frame){
            var msgDivList = getElements(frame, currentTextContainer);

            if (findRes[currentIdx] && findRes[currentIdx].className) {
                findRes[currentIdx].className = findRes[currentIdx].className.replace(" findItemSelected", "");
            }

            for (var i = 0; i < msgDivList.length; i++){
                msgDivList[i].innerHTML = msgDivList[i].innerHTML
                    .split(openSpan).join("")
                    .split(closeSpan).join("");
            }

            hasResult   = false;
            currentText = "";
        }
    }

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

    var _msg = window._messages_.textFinder = {
        find_chat_text  : "find_chat_text",
        find_clear      : "find_clear"
    };

    $rootScope.$on(_msg.find_chat_text, function (e, args) {
        findText.apply(null, args);
    });

    $rootScope.$on(_msg.find_clear, function (e, args) {
        clear.apply(null, args);
    });
}

function servicesController(_projectName) {
    '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 (val){
            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?)$/;

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

            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,19}(\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 serviceList.isString(Src) || serviceList.isArray(Src) ? Src.slice(From, From + (Count || (Src.length - From))) : Src;
        },

        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( "(?:" + EMAIL + "|" + URI1 + "|" + URI2 + ")", "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;
        },

        /*ReplaceURLs:  function (txt, options) {
        options = options || {};

        var default_options = {
            callback: function(text, href) {
                return href ? '<a href="' + href + '" title="' + href + '">' + text + '</a>' : text;
            },
            punct_regexp: /(?:[!?.,:;'"\u00AB\u00BB\u201C\u201D])$/
        };

        // Ініціалізація опцій
        for (var i in default_options) {
            if (options[i] === undefined) {
                options[i] = default_options[i];
            }
        }

        // Екранування спеціальних символів
        function escapeHTML(str) {
            return str.replace(/[&<>"']/g, function(m) {
                return ({
                    '&': '&amp;',
                    '<': '&lt;',
                    '>': '&gt;',
                    '"': '&quot;',
                    "'": '&#39;'
                })[m];
            });
        }

        // Регулярний вираз для URL та email
        var URI_RE = /(?:\b(?:https?|ftp):\/\/[^\s/$.?#].[^\s]*|\bwww\.[^\s]+|[^\s]+@[^\s]+\.[^\s]+)/gi;

        var parts = [];
        var lastIndex = 0;

        txt.replace(URI_RE, function(match, index) {
            var href = match;
            var text = match;
            var prefix = '';

            // Додавання тексту перед посиланням
            if (lastIndex < index) {
                parts.push(txt.substring(lastIndex, index));
            }
            lastIndex = index + match.length;

            // Обрізаємо кінцеву пунктуацію
            while (options.punct_regexp.test(href)) {
                prefix = href.slice(-1) + prefix;
                href = href.slice(0, -1);
                text = text.slice(0, -1);
            }

            // Додаємо схему до посилань без неї
            if (!/^[a-z]+:\/\//i.test(href) && !href.includes('@')) {
                href = 'http://' + href;
            }

            // Якщо це email, додаємо mailto:
            if (/^[^\s]+@[^\s]+\.[^\s]+$/.test(href)) {
                href = 'mailto:' + href;
            }

            // Викликаємо колбек
            var result = options.callback(escapeHTML(text), escapeHTML(href));

            parts.push(result + prefix);
        });

        // Додаємо залишок тексту
        if (lastIndex < txt.length) {
            parts.push(txt.substring(lastIndex));
        }

        return parts.join('');
    },*/

        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 (serviceList.isObject(_data)){
                return _data;
            } else
            if (serviceList.isArray(_data)){
                return _data;
            } else
            if (serviceList.isString(_data)){
                try {
                    res = _data.length ? JSON.parse(_data) : {};
                } catch(e){
                    console.err('[StringToObj] Can\'t convert to JSON, type: ' + (Object.prototype.toString.call( _data )) + ", data: " + serviceList.copy(_data || "", 0, 20) + "...");
                }
            } else {
                console.err('[StringToObj] Can\'t convert to JSON, type: ' + (Object.prototype.toString.call( _data )) + ", data: " + serviceList.copy(_data || "", 0, 20) + "...");
            }

            return res || _data;
        },

        CreateNotification : function (opt) {
            opt = opt || {};

            if (serviceList.notify !== null){
                clearTimeout(serviceList.idNotify);

                try { serviceList.notify.close(); } catch (e) {}
                serviceList.idNotify = 0;
                serviceList.notify = null;
            }

            if (serviceList.noNotify) return;

            var notifOptions = {
                icon: mcConst.imagesPath.all + '75x75.png',
                tag : opt.tag || 'mcNotify',
                body: opt.body || 'Empty'
            };

            // Prefer ServiceWorkerRegistration.showNotification when available
            if (navigator.serviceWorker && navigator.serviceWorker.ready && typeof navigator.serviceWorker.ready.then === 'function'){
                navigator.serviceWorker.ready.then(function(reg){
                    reg.showNotification(opt.title || ' ', notifOptions).then(function(){
                        // try to grab the displayed notification instance so we can close it later
                        try{
                            reg.getNotifications({tag: notifOptions.tag}).then(function(notifs){
                                if (notifs && notifs.length){
                                    serviceList.notify = notifs[notifs.length - 1];

                                    if (opt.onShow) {
                                        try{ serviceList.notify.onshow = opt.onShow; } catch(e){}
                                    }

                                    if (opt.click) {
                                        try{ serviceList.notify.onclick = function(){ window.focus(); opt.click(); }; } catch(e){}
                                    }

                                    serviceList.idNotify = setTimeout(function(){
                                        try{ serviceList.notify.close(); } catch(e){}
                                        serviceList.idNotify = 0;
                                        serviceList.notify = null;
                                    }, opt.timeOut || 5000);
                                }
                            }).catch(function(e){
                                // ignore
                            });
                        } catch (e) {
                            // ignore
                        }
                    }).catch(function(e){
                        console.warn('showNotification failed', e);
                        serviceList.noNotify = true;
                    });
                }).catch(function(e){
                    console.warn('serviceWorker.ready failed', e);
                    serviceList.noNotify = true;
                });

                return;
            }

            // Fallback: try to use the Notification constructor (may throw in some browsers)
            try {
                serviceList.notify = new window.Notification(opt.title || ' ', notifOptions);

                serviceList.idNotify = setTimeout(function(){
                    try{ serviceList.notify.close(); } catch(e){}
                    serviceList.idNotify = 0;
                    serviceList.notify   = null;
                }, opt.timeOut || 5000);

                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){
                    try{ serviceList.notify.onshow = opt.onShow; } catch(e){}
                }

                if (opt.click){
                    try{ serviceList.notify.onclick = function() {
                        clearTimeout(serviceList.idNotify);

                        try{ serviceList.notify.close(); } catch(e){}
                        serviceList.notify = null;

                        serviceList.idNotify = 0;

                        window.focus();

                        opt.click();
                    }; } catch(e){}
                }

                if (serviceList.notify && serviceList.notify.show) {
                    try{ serviceList.notify.show(); } catch(e){}
                }
            } catch (e) {
                console.warn('Notification constructor is not available, use service worker instead', e);
                serviceList.noNotify = true;
            }
        },

        CheckNotification : function (rrr){
            var res = false;

            if (window.webkitNotifications || window.Notification) {
                if ((window.webkitNotifications && window.webkitNotifications.checkPermission && window.webkitNotifications.checkPermission() === 0) || Notification.permission === 'granted') {
                    res = true;
                    serviceList.noNotify = false;
                } else {
                    // Request permission (modern API returns a Promise)
                    try{
                        var p = Notification.requestPermission(function(permission){
                            if(!('permission' in Notification)) Notification.permission = permission;
                        });

                        if (p && typeof p.then === 'function'){
                            p.then(function(permission){
                                if(!('permission' in Notification)) Notification.permission = permission;
                            }).catch(function(){ });
                        }
                    } catch (e) {
                        // some browsers (old webkit) use window.webkitNotifications
                        if (window.webkitNotifications && window.webkitNotifications.requestPermission){
                            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.slice(0, 2).toLowerCase();
            var lang = locale;

            if (locale !== 'en' && locale !== 'ru' && locale !== 'uk') {
                locale = MC_RESOURCE.TextSource.www[_projectName] + 'en';
                lang = 'en';
            } else {
                locale = MC_RESOURCE.TextSource.www[_projectName] + locale;
            }

            // webix.storage.local.put(mcConst.storageFields.lang, lang);
            serviceList.storage.put(mcConst.storageFields.lang, lang);

            return [locale, lang];
        },

        Lang : function(id_or_obj, items, term){
            var opt = {};

            if (serviceList.isString(id_or_obj)){
                return opt.items ? serviceList.myReplace(id_or_obj, items, term) : id_or_obj;
            } else
            if (serviceList.isObject(id_or_obj)){
                opt = {
                    id   : id_or_obj.id,
                    items: id_or_obj.items || [],
                    term : id_or_obj.term  || '%s',
                    html : id_or_obj.html  || false,
                    from : id_or_obj.from  || null
                };
            } else {
                opt = {
                    id   : id_or_obj,
                    items: items || [],
                    term : term  || '%s',
                    html : false,
                    from : null
                }
            }

            var _source = mcConst[opt.from ? "LMSG_" + opt.from : "LMSG_" + _projectName];
            var _res    =  "";

            if (_source[opt.id.toString()]){
                _res = _source[opt.id.toString()];
            } else
            if (serviceList.isString(opt.id)){
                _res = opt.id
            } else {
                console.err("[Lang] unknown param:" + id_or_obj);

                return id_or_obj;
            }

            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 (serviceList.isString(opt.items)) {
                    opt.items = opt.items.replace(/</ig, "&lt;");
                }
            }

            return opt.items ? serviceList.myReplace(_res, opt.items, opt.term) : _res;
        },

        loadTextSource : function (loader, name, cb) {
            if (loader && MC_RESOURCE.TextSource.www[name]) {
                loader(MC_RESOURCE.TextSource.www[name] + mcConst.Lang + '.json').success(function(txt){
                    mcConst["LMSG_" + name] = mcService.StringToObj(txt);
                    cb && cb();
                });
            } else {
                console.err("[loadTextSource] unknown text resource Name: " + name);
            }
        },

        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({ id: 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 : serviceList.Lang({id: i, from: "common"})
                })
            }

            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(100, 343, 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(1, 71, 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({ id: 140 }) // "140":"Ничего не выбрано",
            });

            return res;
        },

        GetPastList : function(){
            var res = serviceList.getLangItemsFromList(546, 553, 1);

            res.unshift({
                id: '-1',
                value: serviceList.Lang({ id: 140 }) // "140":"Ничего не выбрано",
            });

            return res;
        },

        GetPastOrgList : function(){
            var res = serviceList.getLangItemsFromList(554, 573, 1);

            res.unshift({
                id: '-1',
                value: serviceList.Lang({ id: 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, onlyDate){
            var dd = date.split('.');

            return dd[2] + '.' + dd[1] + '.' + dd[0] + (!onlyDate ? ' ' + dd[3] + ':' + dd[4] + (!noSec ? ':' + dd[5] : '') : "");
        },

        secToTime : function(sec, format){
            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 format && serviceList.isString(format)
                ?   (format.indexOf('d') >= 0 ? dd + ':' : "") +
                    (format.indexOf('h') >= 0 ? ((hh < 10) ? ("0" + hh) : hh) + ":" : "") +
                    (format.indexOf('n') >= 0 ? ((nn < 10) ? ("0" + nn) : nn) + ":" : "") +
                    (format.indexOf('s') >= 0 ? ((ss < 10) ? ("0" + ss) : ss) : "")
                : 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({ id: 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({ id: 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") ||
                            (i === "PanicPIN")
                        ) ? "*****" : 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 _key   = /^([A-F0-9]{4}\-){10}[A-F0-9]{4}$/;
            var _promo = /^([A-F0-9]{4}\-){3}[A-F0-9]{4}$/;

            res = _key.test(serviceList.trim(key)) || _promo.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){
                    if (options.actions.beforeAdd()) {
                        $$(_idBFld).hide();
                        $$(_idBtn).show();
                        
                        return;
                    }
                }

                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({ id: 1078, items: len > 15 ? serviceList.copy(selectedItem[options.selectedItem], 0, 15) + "..." : selectedItem[options.selectedItem] }); // "1078":"Удалить %s из списка?",

                    if (serviceList.isArray(selectedItem)){
                        delText = serviceList.Lang({ id: 1142 }); // "1142":"Удалить отмеченные элементы?",
                    }

                    options.regShowWindow(serviceList.RandomHash(4), webix.confirm({
                        type  : "confirm-warning",
                        text  : delText,
                        ok    : serviceList.Lang({ id: 581 }), // "581": "Удалить",
                        cancel: serviceList.Lang({ id: 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({ id: 1506 })); // "1506":"Элементы удалены",
                                } else {
                                    webix.message(serviceList.Lang({ id: 1077, items: 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({ id: 1072 }),// "1072":"Очистить список?",
                    ok    : serviceList.Lang({ id: 114 }), // "114": "Да",
                    cancel: serviceList.Lang({ id: 113 }), // "113": "Нет",
                    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();

                    if (options.listItems[0]) {
                        var _val = selectedItem[options.listItems[0]];

                        items.fields[0].setValue();

                        if (_val !== "30.12.1899.00.00.00" && _val !== "01.01.1900.00.00.00") {
                            items.fields[0].setValue(_val);
                        }

                    } /*selectedItem.IP*/

                    if (options.listItems[1]) items.fields[1].setValue(selectedItem[options.listItems[1]]); /*selectedItem.ClientType*/

                    if (options.listItems[2]) items.fields[2].setValue(selectedItem[options.listItems[2]]); /*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,

                'dateComment'   : 6,
                '6'             : 6
            };

            var options = serviceList.Marge({
                type   : types.textComment,
                idBtn  : serviceList.RandomHash(10),
                gravity: 1,
                listItems: ["IP", "ClientType", "Comment"],
                actions: {
                    del    : null,
                    clear  : null,
                    save   : null,
                    preview: null,
                    enable : null,
                    disable: null,
                    beforeAdd: null,
                    afterClose: null
                },
                text : {
                    field1 : "", // "1075":"Введите IP, маску или диапазон",
                    field2 : "", // serviceList.Lang({ id: { id: 1032 }}) "1032":"Комментарий:",
                    field3 : ""  // serviceList.Lang({ id: { id: 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
                };
                var nowDate = new Date();

                res = { gravity: options.gravity, rows : [
                    { id: _idBtn, cols: [] // блок с кнопками: добавить удалить очистить
                        .concat([{ view: "button", value: options.titleBtns.add || serviceList.Lang({ id: 50 }), hidden: !options.visibleBtns.add, click: add, css: "applyButton"}]) // "50" : "Добавить",
                        .concat([{ view: "button", value: options.titleBtns.del || serviceList.Lang({ id: 52 }), hidden: !options.visibleBtns.del, click: del, type: "danger"}])     // "52" : "Удалить",
                        .concat(types[options.type] === types.textRichselectCommentEdit ? [{ view: "button", value: serviceList.Lang({ id: 51 }), hidden: !options.visibleBtns.del, click: edit}] : [])    // "51" : "Изменить",
                        .concat(options.visibleBtns.cls ? [{ view: "button", value: options.titleBtns.cls || serviceList.Lang({ id: 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); } } },

                            { height: 4},

                            { 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({ id: 104 }), width: 100, click: save}, // "104": "Сохранить",
                                { view: "button", value: options.titleBtns.cancel || serviceList.Lang({ id: 103 }), width: 100, click: back, type: "danger"}  // "103": "Отмена",
                            ]}
                        ];
                    break;

                    case types.dateComment:
                        res.rows[1].rows = [
                            {   view: "datepicker", format:"%d.%m.%Y", label: options.text.field1, css:"cPointer", labelWidth: 230, //  placeholder: options.text.field1, value: nowDate,
                                suggest:{
                                    type:"calendar",
                                    body:{
                                        minDate: new Date().getTime() - mcConst.oneDay
                                    }
                                },
                                on: { onAfterRender: function() { items.fields[0] = this; afterRender.apply(this, arguments); } } },

                            { height: 4},

                            { 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({ id: 104 }), width: 100, click: save}, // "104": "Сохранить",
                                { view: "button", value: options.titleBtns.cancel || serviceList.Lang({ id: 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({ id: 104 }), width: 100, click: save}, // "104": "Сохранить",
                            { view: "button", value: options.titleBtns.cancel || serviceList.Lang({ id: 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({ id: 104 }), width: 100, click: save, css: "applyButton"}, // "104": "Сохранить",
                            { view: "button", value: options.titleBtns.cancel || serviceList.Lang({ id: 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 } } }
                            ]},

                            { height: 4},

                            { 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({ id: 104 }), width: 100, click: save}, // "104": "Сохранить",
                                { view: "button", value: options.titleBtns.cancel || serviceList.Lang({ id: 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
                                    } } }
                            ]},

                            { height: 4},

                            { 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({ id: 104 }), width: 100, click: function () {save(types[options.type])}}, // "104": "Сохранить",
                                { view: "button", value: options.titleBtns.cancel || serviceList.Lang({ id: 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, onlyCustomBtn){
            function clearSelection() {
                master.blockEvent();

                if (master.unselectAll){
                    master.unselectAll()
                } else {
                    master.clearSelection();
                }

                master.unblockEvent();
                master.callEvent("onAfterSelect");
            }

            function selectAll (){
                master.blockEvent();

                if (master.openAll){
                    master.openAll();
                }

                // clearSelection();

                if (master.selectAll){
                    master.selectAll();
                } else {
                    master.selectRange(master.getFirstId(), master.getLastId());
                }

                master.unblockEvent();
                master.callEvent("onAfterSelect");
            }

            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) {
                            var selectedItems = master.getSelectedItem(true);
                            
                            onShow.apply(this, [selectedItems]);
                        }
                    }
                }
            } : [];

            var items = (onlyCustomBtn ? [] : [
                { view: "button", value: serviceList.Lang({ id: 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({ id: 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({ id: 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();

                    master.blockEvent();

                    for (var i = 0; selectedItems && i < selectedItems.length; i ++){
                        master.unselect(selectedItems[i].id);
                    }

                    master.unblockEvent();
                    master.callEvent("onAfterSelect");

                    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({ id: 10 });   break; // "10" : "Информация о сервере",
                case "UserManager":     res = serviceList.Lang({ id: 49 });   break; // "49" : "Список пользователей",
                case "UserProfile":     res = serviceList.Lang({ id: 1484 }); break; // "1484":"Профиль пользователя",
                case "GroupRights":     res = serviceList.Lang({ id: 57 });   break; // "57" : "Группы прав"
                case "GroupUsers":      res = serviceList.Lang({ id: 916 });  break; // "916":"Пользователи групп",
                case "Contacts":        res = serviceList.Lang({ id: 587 });  break; // "587": "Общий список контактов",
                case "WorkPositions":   res = serviceList.Lang({ id: 588 });  break; // "588": "Должности компании",
                case "WebSupport":      res = serviceList.Lang({ id: 938 });  break; // "938":"Web-поддержка",
                case "MyChatGuest":     res = serviceList.Lang({ id: 945 });  break; // "945":"MyChat Guest",
                case "LicenseMyChat":   res = serviceList.Lang({ id: 944 });  break; // "944":"MyChat Server",
                case "LicenseGuest":    res = serviceList.Lang({ id: 945 });  break; // "945":"MyChat Guest",
                case "UsersImports":    res = serviceList.Lang({ id: 960 });  break; // "960":"Импорт пользователей",
                case "Operators":       res = serviceList.Lang({ id: 817 });  break; // "817":"Модераторы",
                case "Bans":            res = serviceList.Lang({ id: 1029 }); break; // "1029":"Забаненные пользователи",
                case "AutoChannels":    res = serviceList.Lang({ id: 1039 }); break; // "1039":"Автосоздаваемые конференции",
                case "RemoteSettings":  res = serviceList.Lang({ id: 1329 }); break; // "1329":"Дистанционные настройки клиента",
                case "History":         res = serviceList.Lang({ id: 1134 }); break; // "1134":"Просмотр протоколов",
                case "FilterAntiflood": res = serviceList.Lang({ id: 1057 }); break; // "1057":"Антифлуд",
                case "FilterBadWords":  res = serviceList.Lang({ id: 1058 }); break; // "1058":"Плохие слова",
                case "FilterIP":        res = serviceList.Lang({ id: 1059 }); break; // "1059":"IP фильтры",
                case "FilterMAC":       res = serviceList.Lang({ id: 1060 }); break; // "1060":"MAC фильтры",
                case "FilterNames":     res = serviceList.Lang({ id: 1061 }); break; // "1061":"Блокировки названий",
                case "SettingsNetwork": res = serviceList.Lang({ id: 591 });  break; // "591": "Сетевые настройки MyChat",
                case "SettingsInfo":    res = serviceList.Lang({ id: 596 });  break; // "596": "Информация",
                case "SettingsLogs":    res = serviceList.Lang({ id: 598 });  break; // "598": "Протоколирование",
                case "SettingsFTP":     res = serviceList.Lang({ id: 599 });  break; // "599": "FTP файловый сервер",
                case "SettingsWEB":     res = serviceList.Lang({ id: 600 });  break; // "600": "WEB-сервисы",
                case "SettingsBackup":  res = serviceList.Lang({ id: 601 });  break; // "601": "Резервные копии",
                case "SettingsService": res = serviceList.Lang({ id: 602 });  break; // "602": "Обслуживание",
                case "SettingsAPI":     res = serviceList.Lang({ id: 603 });  break; // "603": "Integration API",
                case "SettingsSMTP":    res = serviceList.Lang({ id: 921 });  break; // "921":"Настройка SMTP",
                case "Others":          res = serviceList.Lang({ id: 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({ id: 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, short){
            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, !!short);
                        } else {
                            view.text(serviceList.MyFormatTime(Now, !!short));
                        }
                    }
                }, 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'>" + serviceList.Lang({ id: 1552 }) + '</span><br>';// "1552":"Добавлены пользователи:",
                    res += add.join('<br>') + '<br><br>';
                }

                if (upd.length){
                    res += "<span class='green _grName'>" + serviceList.Lang({ id: 1553 }) + '</span><br>';// "1553":"Обновлены пользователи:",
                    res += upd.join('<br>') + '<br><br>';
                }

                if (err.length){
                    res += "<span class='red _grName'>" + serviceList.Lang({ id: 1554 }) + '</span><br>';// "1554":"Не добавленные пользователи:",
                    res += err.join('<br>');
                }

                var wnd = webix.ui({
                    view: "window",
                    position: "center",
                    modal: true,
                    width: 300,
                    height: 500,
                    head: serviceList.Lang({ id: 971 }), // "971":"Результат:",
                    body: { rows: [
                        { template: res, borderless: true, scroll: "y" },
                        { view: "button", value: serviceList.Lang({ id: 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({ id: 1641 }); break; // "1641":"Не в сети",

                case mcConst.states.online:
                    state = (!noColor ? '<span class="green">' : "") + serviceList.Lang({ id: 1642 }) + (!noColor ? '</span>' : ""); break; // "1642":"В сети",

                case mcConst.states.webOnline:
                    state = (!noColor ? '<span class="green">' : "") + serviceList.Lang({ id: 1642 }) + (!noColor ? '</span>' : ""); break; // "1642":"В сети",

                case mcConst.states.away:
                    state = (!noColor ? '<span class="blue">' : "") + serviceList.Lang({ id: 1643 }) + (!noColor ? '</span>' : ""); break; // "1643":"Нет на месте",

                case mcConst.states.dnd:
                    state = (!noColor ? '<span class="red">' : "") + serviceList.Lang({ id: 1644 }) + (!noColor ? '</span>' : ""); break; // "1644":"Не беспокоить",
            }

            return state;
        },

        getColorForStatus: function(status){
            status = parseInt(status);

            switch (status){
                case mcConst.states.offline:   return 'statusOffline';  // "29" : "Не в сети",
                case mcConst.states.online:    return 'statusOnline';   // "30" : "В сети",
                case mcConst.states.webOnline: return 'statusOnline';   // "30" : "В сети",
                case mcConst.states.away:      return 'statusAway';     // "31" : "Нет на месте",
                case mcConst.states.dnd:       return 'statusDnd';      // "32" : "Не беспокоить",
                default:                       return ''; //'statusDefault';
            }
        },

        getUserColorByStatus: function(status){
            status = parseInt(status);

            switch (status){
                case mcConst.states.offline:      // "29" : "Не в сети",
                    return 'userStatusOffline';

                case mcConst.states.online:       // "30" : "В сети",
                case mcConst.states.webOnline:    // "30" : "В сети",
                case mcConst.states.away:         // "31" : "Нет на месте",
                case mcConst.states.dnd:          // "32" : "Не беспокоить",
                    return 'userStatusOnline';
            }
        },

        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 (serviceList.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].toLowerCase();
        },

        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 = serviceList.Lang({ id: 61 }); break;
                case mcConst.priority.MEDIUM   : res = serviceList.Lang({ id: 62 }); break;
                case mcConst.priority.HIGH     : res = serviceList.Lang({ id: 63 }); break;
                case mcConst.priority.IMPORTANT: res = serviceList.Lang({ id: 64 }); break;
                case mcConst.priority.URGENT   : res = serviceList.Lang({ id: 65 }); break;
                case mcConst.priority.CRITICAL : res = serviceList.Lang({ id: 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);
            var newClass     = serviceList.getColorForStatus(state);
            var statusClasses = ['statusOnline', 'statusOffline', 'statusAway', 'statusDnd', 'statusDefault'];

            for (var i = 0; i < users.length; i++){
                for (var j = 0; j < statusClasses.length; j++){
                    users[i].classList.remove(statusClasses[j]);
                }
                users[i].classList.add(newClass);
            }
        },

        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.SysEventsEnableEmotions == '0')){
                                    items[idx] = `<img src='${MC_RESOURCE.TextSource.smileys + mcConst.SmileysSet[elem[0]].name}/${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    : isComment ? "auto" : resolutionW || "",
                                    height   : isComment ? "auto" : resolutionH || "",
                                    widthPX  : "width:"  + (isComment ? "auto" : (resolutionW && resolutionW !== "auto" ? resolutionW + "px": resolutionW)) + ";",
                                    heightPX : "height:" + (isComment ? "auto" : (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;
        },

        getMyStatusTxt: function (state) {
            switch (state || mcConst.UserInfo.onlineState) {
                case mcConst.states.online   : return serviceList.Lang({ id: 30 }); // "30" :"В сети",
                case mcConst.states.offline  : return serviceList.Lang({ id: 29 }); // "29" :"Не в сети",
                case mcConst.states.away     : return serviceList.Lang({ id: 31 }); // "31" :"Нет на месте",
                case mcConst.states.dnd      : return serviceList.Lang({ id: 32 }); // "32" :"Не беспокоить",
                case mcConst.states.webOnline: return serviceList.Lang({ id: 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;
        },

        getOrientation: function (file, callback) {
            //Provided a image file, determines the orientation of the file based on the EXIF information.
            //Calls the "callback" function with an index into the JpegOrientation array.
            //If the image is not a JPEG, returns 0. If  the orientation value cannot be read (corrupted file?) return -1.

            const reader = new FileReader();
            reader.onload = (e) => {

                const view = new DataView(e.target.result);

                if (view.getUint16(0, false) !== 0xFFD8) {
                    return callback(0);  //NOT A JPEG FILE
                }

                const length = view.byteLength;
                let offset = 2;
                while (offset < length) {

                    if (view.getUint16(offset+2, false) <= 8)   //unknown?
                        return callback(-1);

                    const marker = view.getUint16(offset, false);
                    offset += 2;
                    if (marker === 0xFFE1) {

                        if (view.getUint32(offset += 2, false) !== 0x45786966)
                            return callback(-1); //unknown?


                        const little = view.getUint16(offset += 6, false) === 0x4949;
                        offset += view.getUint32(offset + 4, little);
                        const tags = view.getUint16(offset, little);
                        offset += 2;
                        for (var i = 0; i < tags; i++) {
                            if (view.getUint16(offset + (i * 12), little) === 0x0112) {
                                return callback(view.getUint16(offset + (i * 12) + 8, little));   //found orientation code
                            }
                        }
                    }
                    else if ((marker & 0xFF00) !== 0xFF00) {
                        break;
                    }
                    else {
                        offset += view.getUint16(offset, false);
                    }
                }

                return callback(-1); //unknown?
            };
            reader.readAsArrayBuffer(file);
        },

        resetOrientation: function (srcBase64, srcOrientation, callback, type = "image/jpeg") {
            //Takes a jpeg image file as base64 and transforms it back to original, providing the
            //transformed image in callback.  If the image is not a jpeg or is already in normal orientation,
            //just calls the callback directly with the source.
            //Set type to the desired output type if transformed, default is image/jpeg for speed.

            if (srcOrientation <= 1) {  //no transform needed
                callback(srcBase64);
                return;
            }

            const img = new Image();

            img.onload = () => {
                const width = img.width;
                const height = img.height;
                const canvas = document.createElement('canvas');
                const ctx = canvas.getContext("2d");

                // set proper canvas dimensions before transform & export
                if (4 < srcOrientation && srcOrientation < 9) {
                    canvas.width = height;
                    canvas.height = width;
                } else {
                    canvas.width = width;
                    canvas.height = height;
                }

                // transform context before drawing image
                switch (srcOrientation) {
                    case 2: ctx.transform(-1, 0, 0, 1, width, 0);       break;
                    case 3: ctx.transform(-1, 0, 0, -1, width, height); break;
                    case 4: ctx.transform(1, 0, 0, -1, 0, height);      break;
                    case 5: ctx.transform(0, 1, 1, 0, 0, 0);            break;
                    case 6: ctx.transform(0, 1, -1, 0, height, 0);      break;
                    case 7: ctx.transform(0, -1, -1, 0, height, width); break;
                    case 8: ctx.transform(0, -1, 1, 0, 0, width);       break;
                    default: break;
                }

                // draw image
                ctx.drawImage(img, 0, 0);

                //export base64
                callback(canvas.toDataURL(type), srcOrientation);
            };

            img.src = srcBase64;
        },

        isUserConnect: function (type) {
            var res = false;

            if (type === mcConst._CMD_.ClientType.WIN32 ||
                type === mcConst._CMD_.ClientType.WIN32_NEXT ||
                type === mcConst._CMD_.ClientType.ANDROID ||
                type === mcConst._CMD_.ClientType.IOS ||
                type === mcConst._CMD_.ClientType.LINUX ||
                type === mcConst._CMD_.ClientType.MACOS ||
                type === mcConst._CMD_.ClientType.WEB_SUPPORT ||
                type === mcConst._CMD_.ClientType.WEB_CHAT ||
                type === mcConst._CMD_.ClientType.GUEST) {
                res = true;
            }

            return res;
        },

        insertSmileyImg: function (smileyCode, addClass) {
            var _code = smileyCode.toString(16).toUpperCase();
            
            return `<img src="${MC_RESOURCE.TextSource.smileys}emoji/${_code}.png" code="${_code}" class="emojiSize ${addClass ? addClass : ''}">`;
        },

        transliterate: function (text) {
            const toLatMap = {
                'а': 'a',  'б': 'b', 'в': 'v',  'г': 'g',
                'ґ': 'g',  'д': 'd', 'е': 'e',  'є': 'ye',
                'ж': 'zh', 'з': 'z', 'и': 'y',  'і': 'i',
                'ї': 'yi', 'й': 'y', 'к': 'k',  'л': 'l',
                'м': 'm',  'н': 'n', 'о': 'o',  'п': 'p',
                'р': 'r',  'с': 's', 'т': 't',  'у': 'u',
                'ф': 'f',  'х': 'h', 'ц': 'ts', 'ч': 'ch',
                'ш': 'sh', 'щ': 'sch', 'ь': '', 'ю': 'yu',
                'я': 'ya', 'ы': 'i',
            };
            
            return text.split('').map(char => {
                const lowerChar = char.toLowerCase();
                let res = "";

                if (toLatMap[lowerChar] !== undefined) {
                    res = char === lowerChar
                        ? toLatMap[lowerChar]
                        : toLatMap[lowerChar].toUpperCase();
                } else {
                    res = char;
                }

                return res;
            }).join('');
        },

        daysCountName: function (days, label) {
            var stDays = "";
            var lblTxt = function (i) {
                return label[i] === void(0) ? "days" : serviceList.isString(label[i]) ? label[i] : serviceList.Lang({ id: label[i] })
            };

            if ((days >=5 && days <=19) || (days % 10 >=5 && days % 10 <=9) || (days % 10 === 0)) {
                stDays = lblTxt(2); // "186":"дней",
            } else
            if (days % 10 === 1) {
                stDays = lblTxt(0); // "184":"день",
            } else
            if (days % 10 >=2 && days % 10 <=4) {
                stDays = lblTxt(1); // "185":"дня",
            } 

            return stDays;
        },

        displayError: function ($scope, txtCode, txt, txtParam, callback, width){
            $scope.errorMessagesIsShowed = true;

            var opt = txtParam === "nohelp"
                ? {
                    type   : "alert",
                    ok     : serviceList.Lang({ id: 362, from: "common" }), // common "362" : "Закрыть",
                    padding: 12,
                    text   : serviceList.myReplace(txt, [].concat(txtParam)),
                    callback: function (){
                        $scope.errorMessagesIsShowed = false;
                    }
                }
                : {
                    title  : txtCode === 'client'
                        ? serviceList.Lang({ id: 366, from: "common" }) // common "366" : "Ошибка",
                        : serviceList.Lang({ id: 361, from: "common", items: txtCode }), // common "361" : "MyChat Server: ошибка №%s",
                    ok     : serviceList.Lang({ id: 365, from: "common" }),  // common "365" : "Помощь",
                    cancel : serviceList.Lang({ id: 362, from: "common" }), // common "362" : "Закрыть",
                    type   : "alert-error",
                    padding: 12,
                    text   : serviceList.myReplace(txt, [].concat(txtParam)),
                    callback: function(res) {
                        if (res){
                            if (txtCode === 'client') {
                                serviceList.goToHelp("/serverhelp/", true);
                            } else {
                                var code = ['0','0','0','0'];
                                var cLen = txtCode.toString().length - 1;
                                var anchor = "";

                                for (var i = 3; i >= 0; i--){
                                    if (cLen < 0) {
                                        break;
                                    }

                                    code[i] = txtCode.toString()[cLen];
                                    cLen --;
                                }

                                code = code.join('');

                                if (code == "0165"){
                                    anchor = "#" + txtParam[0];
                                }

                                if (txtCode == mcConst._CMD_.Errors.Disconnected) {
                                    serviceList.goToHelp("https://nsoft-s.com/" + (mcConst.Lang === "ru" ? "" : (mcConst.Lang === "uk" ? "ua/" : "en/")) + "mcserverhelp/mychatclienterrors" + code + ".html" + anchor, true);
                                } else {
                                    serviceList.goToHelp("/serverhelp/" + (mcConst.Lang === "uk" ? "/ua" : "/" + mcConst.Lang) + "/mychatclienterrors" + code + ".html" + anchor, true);
                                }
                            }
                        }

                        if (callback){
                            callback(res);
                        }

                        var args = serviceList.isArray($scope.errorMessages) ? $scope.errorMessages.shift() : void(0);

                        if (args !== undefined){
                            setTimeout(function(){
                                serviceList.displayError.apply(null, args);
                            }, 20);
                        } else {
                            $scope.errorMessagesIsShowed = false;
                        }

                        return res;
                    }
                };

            if (width) opt.width = width;

            webix[txtParam === "nohelp" ? "alert" : "confirm"](opt);
        }
    };

    var multiple    = {
        kanban: {
            FilterFiled : function (options){
                function clearFilter(filter, noFocus){
                    if (options.thisFilter){
                        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 filterParams = [];
                    var show = true;

                    for (var i = 0; i < paramList.length; i++){
                        var param = paramList[i];

                        if (serviceList.isArray(obj[param])){
                            filterParams.push(obj[param].join(' ').toLowerCase());
                        } else
                        if (param.indexOf(':') === -1){
                            if (obj[param] !== undefined) filterParams.push(obj[param].toString().toLowerCase());
                        } else {
                            var _param = param.split(':');

                            filterParams = filterParams.concat(obj[_param[0]].toString().toLowerCase().split(_param[1]));
                        }
                    }

                    if (serviceList.isString(text)){
                        if (strict){
                            show = filterParams.indexOf(text.toString().toLowerCase()) !== -1;
                        } else {
                            var haystack = filterParams.join(' ');

                            // exact match first (fast path)
                            show = haystack.indexOf(text) !== -1;

                            // fuzzy fallback via Fuse.js
                            if (!show && window.Fuse) {
                                var fuseData    = [{ val: haystack }];
                                var fuseOptions = {
                                    keys            : ['val'],
                                    threshold       : 0.34,
                                    ignoreLocation  : true,
                                    useExtendedSearch: true,
                                    tokenize        : true,
                                    includeScore    : true,
                                    findAllMatches  : true
                                };

                                var fuse            = new Fuse(fuseData, fuseOptions);
                                var queryTokens     = text.split(/\s+/).filter(Boolean);
                                var extendedQueryAND = { $and: queryTokens.map(function(t){ return { val: t }; }) };
                                var extendedQueryOR  = { $or:  queryTokens.map(function(t){ return { val: t }; }) };

                                var results = fuse.search(extendedQueryAND);

                                if (results.length === 0) {
                                    results = fuse.search(extendedQueryOR);

                                    if (results.length === 0) {
                                        results = fuse.search(text);
                                    }
                                }

                                show = results.length > 0;
                            }
                        }
                    } else

                    if (serviceList.isBoolean(text)){
                        show = filterParams.indexOf(text.toString().toLowerCase()) !== -1;
                    } else

                    if (serviceList.isArray(text)){
                        for (var i = 0; i < text.length; i ++){
                            if (filterParams.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());

                            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 && !subFilter){
                                                    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({ id: 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 (options.thisFilter){
                        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);
                                });

                                options.List.showItem(options.List.getFirstId());

                                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({ id: 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 }
                ], on: {
                    onAfterRender: function f() {
                        // this.doFilter = doFilter;
                        // this.clearFilter = clearFilter;
                    }}
                };

                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 (options.thisFilter) {
                        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({ id: 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 (_projectName && multiple[_projectName]){
        Object.keys(multiple[_projectName]).forEach(function (keyName) {
            serviceList[keyName] = multiple[_projectName][keyName];
        });
    }

    return serviceList;
}

var JpegOrientation = [
    "NOT_JPEG",
    "NORMAL",
    "FLIP-HORIZ",
    "ROT180",
    "FLIP-HORIZ-ROT180",
    "FLIP-HORIZ-ROT270",
    "ROT270",
    "FLIP-HORIZ-ROT90",
    "ROT90"
];

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
    });
}
// McMessageParser — спільний парсер контенту повідомлень для chat та admin
// Без DOM-маніпуляцій (крім barcode), без фреймів, нотифікацій, звуків
//
// Залежності (глобальні): mcService, mcConst, mcLang
// Опціональні глобальні: QRCode, webix (для barcode), hljs (для code snippets)

class MyRegExp extends RegExp {
    [Symbol.split](str, limit) {
        var result = RegExp.prototype[Symbol.split].call(this, str, limit);
        return result.reduce(function (accumulator, currentValue) {
            if (!accumulator) {
                accumulator = [];
            } else
            if (mcService.isString(accumulator)){
                accumulator = [accumulator];
            }

            if (currentValue) {
                accumulator.push(`${currentValue}`);
            }

            return accumulator;
        });
    }
}

function McMessageParser(options) {
    "use strict";

    var Self       = this;
    var emojiList  = options.emojiList || [];
    var isWeb      = options.isWebClient || false;
    var thumbsSize = options.thumbsSize || {x: 400, y: 400};
    var context    = options.context || 'chat'; // 'chat' | 'admin'
    var listCE     = (typeof CallStates !== 'undefined') ? new CallStates().list : {};

    // -- callbacks --
    var getDisplayName  = options.getDisplayName  || function(uin){ return "User " + uin; };
    var getLocalHostPath= options.getLocalHostPath|| function(){ return ""; };
    var onLoadImage     = options.onLoadImage     || null; // fn(hash, idx)
    var onLoadImageNew  = options.onLoadImageNew  || null; // fn(info, idx)
    var colorPalette    = options.colorPalette    || [];
    var getCurrentUIN   = options.getCurrentUIN   || function(){ return 0; };

    // -----------------------------------------------
    // Утиліти
    // -----------------------------------------------

    function clearText(txt) {
        return txt.replace(/&/ig, "&amp;").replace(/</ig, "&lt;");
    }

    function parseEmoji(text) {
        var symbol   = "";
        var result   = "";

        for (var i = 0; i < text.length; i++){
            symbol = text.codePointAt(i);

            if (symbol >= 0x1F004 && symbol <= 0x1F9C0 && mcService.inArray(symbol, emojiList) >= 0) {
                result += mcService.insertSmileyImg(symbol);
                i++;
            } else
            if (symbol >= 0x203C && symbol <= 0x3299 && mcService.inArray(symbol, emojiList) >= 0) {
                result += mcService.insertSmileyImg(symbol);
            } else
            if (symbol === 0x00A9 || symbol === 0x00AE){
                result += mcService.insertSmileyImg(symbol);
            } else {
                result += text[i];
            }
        }

        return result;
    }

    function urlCallback(_text, _link) {
        var res = "";

        if (/(\\.png|\\.jpg|\\.jpeg)$/.test(_link) && _link.indexOf(getLocalHostPath()) === 0){
            res = '<br/><img class="thumbImage" src="' + _link + '">';
        }

        var desc = "";
        try { desc = decodeURI(res || _text); } catch (e) { desc = res || _text; }

        return _link ? '<a href="' + _link + '" title="' + _link + '" target="_blank">' + desc + '</a>' : _text;
    }

    function urlCallbackChat(_text, _link, msgIdx, total) {
        var res  = "";
        var data = _link ? _link.split("/") : [];

        if (_link && data.length >= 4 && (data[3] === "p" || data[3] === "reel") && (_link.indexOf(mcConst.linkType.instagram1) >= 0 || _link.indexOf(mcConst.linkType.instagram2) >= 0)) {
            var desc2 = "";

            try {
                desc2 = decodeURI(_text);
            } catch(e) {
                desc2 = _text;
            }

            _text = `<a href="${_link}" title="${_link}" target="_blank">${desc2}</a><span id="instagram-media-payload-${msgIdx}"></span> ` +
                `<iframe className="instagram-media instagram-media-rendered" id="instagram-embed-${msgIdx}" `+
                `src="https://instagram.com/${data[3]}/${data[4]}/embed/captioned" `+
                `allowTransparency="true" allowFullScreen="true" frameBorder="0" height="500" `+
                `data-instgrm-payload-id="instagram-media-payload-${msgIdx}" scrolling="no" ` +
                `style="background: transparent; max-width: 350px; width: calc(100% - 2px); border-radius: 5px; border: 0px solid transparent; box-shadow: none; display: block; margin: 0px 0px 12px; min-width: 326px; padding: 0px;"></iframe>`;
        } else

        if (_link && !total.twitter && (_link.indexOf(mcConst.linkType.twitter1) > 0 || _link.indexOf(mcConst.linkType.twitter2) > 0) && data.length > 4) {
            _text = `<span id="twttr-${msgIdx}" className="twitter-tweet">`+
                       `<a href="${_link}">${_link}</a>`+
                    `</span>`;

            total.twitter = true;
            total.twId = _link.split('/');
            total.twId = total.twId[total.twId.length - 1];
            total.twId = total.twId.split('?')[0];

            _link = false;
        } else

        if (_link && _link.indexOf(mcConst.myChatLinkTitle) > 0){
            _link = _link.split(mcConst.myChatLinkTitle);
            res   = _link[1];
            _link = _link[0];
        } else

        if (/(\\.png|\\.jpg|\\.jpeg)$/.test(_link) && _link.indexOf(getLocalHostPath()) === 0){
            res = '<br/><img class="thumbImage" src="' + _link + '">';
        } else

        if (_link && typeof mcConst.ftpPersonal !== 'undefined' && _link.indexOf(mcConst.ftpPersonal) === 0){
            _link = "";
            _text = "FTP Personal";
        } else
        if (_link && typeof mcConst.ftpPublic !== 'undefined' && _link.indexOf(mcConst.ftpPublic) === 0){
            _link = "";
            _text = "FTP Public";
        }

        var desc = "";
        try { desc = decodeURI(res || _text); } catch (e) { desc = res || _text; }

        return _link ? '<a href="' + _link + '" title="' + _link + '" target="_blank">' + desc + '</a>' : _text;
    }

    // -----------------------------------------------
    // Парсери по типах повідомлень
    // -----------------------------------------------

    function parseText(msg, info) {
        var text = msg.MsgType ? msg.Msg.split(mcConst.CRLF) : msg.Msg.split('L$');

        if (!msg.MsgType) {
            text.shift();
        }

        for (var i = 0; i < text.length; i++){
            text[i] = clearText(text[i]);
            if (!msg.MsgType) {
                text[i] = text[i].replace(/\u000b/ig, "");
            }
        }

        text = text.join('<br/>');

        var notifyText = text;

        text = mcService.replaceMcComponents(text, isWeb, null, info.nick);

        var total = { twitter: false, twId: null };

        if (context === 'chat') {
            text = mcService.ReplaceURLs(text, { callback: function(_text, _link){
                return urlCallbackChat(_text, _link, msg.Idx, total);
            }});
        } else {
            text = mcService.ReplaceURLs(text, { callback: urlCallback });
        }

        text = parseEmoji(text);

        return {
            html: text,
            notifyText: notifyText,
            flags: { twitter: total.twitter, twId: total.twId }
        };
    }

    function parseLink(msg, info) {
        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) {
            if (msg.MsgType === mcConst._CMD_.msgType.LAN_LINK && link.Link.indexOf(link.Desc) !== -1){
                link.Desc = (context === 'chat' ? mcLang({ id: 669 , from: "common"}) : "Local lan link: ") + link.Link;
            } 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; }

        if (mcService.isObject(link.Link)){
            link.Link = mcService.ObjToString(link.Link);
        }

        var text = "";
        var flags = { twitter: false, twId: null };

        if (context === 'chat') {
            var data = link.Link ? link.Link.split("/") : [];

            if (link.Link && data.length >=4 && (data[3] === "p" || data[3] == "reel") && (link.Link.indexOf(mcConst.linkType.instagram1) >= 0 || link.Link.indexOf(mcConst.linkType.instagram2) >= 0)) {
                text = `<a href='${link.Link}' title='${desc}${local}' target='_blank'>${desc}</a><span id="instagram-media-payload-${msg.Idx}"></span>`+
                    `<iframe className="instagram-media instagram-media-rendered" id="instagram-embed-${msg.Idx}" `+
                    `src="https://instagram.com/${data[3]}/${data[4]}/embed/captioned" `+
                    `allowTransparency="true" allowFullScreen="true" frameBorder="0" height="500" `+
                    `data-instgrm-payload-id="instagram-media-payload-${msg.Idx}" scrolling="no" ` +
                    `style="background: transparent; max-width: 350px; width: calc(100% - 2px); border-radius: 5px; border: 0px solid transparent; box-shadow: none; display: block; margin: 0px 0px 12px; min-width: 326px; padding: 0px;"></iframe>`;
            } else
            if (link.Link && (link.Link.indexOf(mcConst.linkType.twitter1) > 0 || link.Link.indexOf(mcConst.linkType.twitter2) > 0) && data.length > 4) {
                text = `<span id="twttr-${msg.Idx}" className="twitter-tweet">`+
                            `<a href="${link.Link}">${link.Link}</a>`+
                         `</span>`;
                flags.twitter = true;
                flags.twId = link.Link.split('/');
                flags.twId = flags.twId[flags.twId.length - 1];
                flags.twId = flags.twId.split('?')[0];
            } else {
                text = `<a href='${link.Link}' title='${link.Link}' target='_blank'>${desc}</a>`;
            }
        } else {
            text = mcService.myReplaceFormated(
                "<a href='#{link}' title='#{title}#{local}' target='_blank'>#{title}</a>",
                { link: link.Link, title: link.Desc, local: local }
            );
        }

        return {
            html: text,
            notifyText: context === 'chat' ? mcLang({ id: 668 , from: "common"}) : "",
            flags: flags
        };
    }

    function parseImageOld(msg, info) {
        var msgInfo    = msg.Msg.split(mcConst.CRLF);
        var hash       = msgInfo[0];
        var resolutionW = msgInfo[4] && msgInfo[4] !== "-" ? Math.round(parseInt(msgInfo[4].split('x')[0])) : "auto";
        var resolutionH = msgInfo[4] && msgInfo[4] !== "-" ? Math.round(parseInt(msgInfo[4].split('x')[1])) : "auto";
        var idx        = hash + info.msgIdx;
        var loadImage  = mcConst.imagesPath.loadImage;
        var dt         = msgInfo[2];

        if (msgInfo[4]){
            if (resolutionH >= resolutionW) {
                if (resolutionH > thumbsSize.y) resolutionH = thumbsSize.y;
                resolutionW = "auto";
            } else {
                if (resolutionW > thumbsSize.x) resolutionW = thumbsSize.x;
                resolutionH = "auto";
            }
        }

        var height = resolutionH !== "auto" ? resolutionH + "px" : resolutionH;
        var width  = resolutionW !== "auto" ? resolutionW + "px" : resolutionW;

        var text;

        if (context === 'chat') {
            text = `<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="${info.msgIdx}" fileName="${msgInfo[1]}">`+
                   `</a>`;
        } else {
            text = mcService.myReplaceFormated(
                '<a id="link_#{idx}" href="" target="_blank"><img id="img_#{idx}" class="messageImg" src="#{loadImage}" dt="#{dt}" alt=""></a>',
                { idx: idx, loadImage: loadImage, dt: dt }
            );
        }

        if (onLoadImage) onLoadImage(hash, info.msgIdx);

        return {
            html: text,
            notifyText: mcLang({ id: 570 , from: "common"}),
            flags: { loadImage: true, hash: hash }
        };
    }

    function parseImageNew(msg, info) {
        var imgInfo = mcService.StringToObj(msg.Msg.split(mcConst.CRLF)[0]);

        if (mcService.isString(imgInfo)) {
            return parseImageOld(msg, info);
        }

        var hash       = imgInfo.Hash;
        var resolutionW = imgInfo.Resolution && imgInfo.Resolution !== "-" ? Math.round(parseInt(imgInfo.Resolution.split('x')[0])) : "auto";
        var resolutionH = imgInfo.Resolution && imgInfo.Resolution !== "-" ? Math.round(parseInt(imgInfo.Resolution.split('x')[1])) : "auto";
        var idx        = hash + info.msgIdx;
        var loadImage  = mcConst.imagesPath.loadImage;
        var dt         = imgInfo.UTCWriteTime;
        var fileName   = imgInfo.FileName;

        if (imgInfo.Resolution){
            if (resolutionH >= resolutionW) {
                if (resolutionH > thumbsSize.y) resolutionH = thumbsSize.y;
                resolutionW = "auto";
            } else {
                if (resolutionW > thumbsSize.x) resolutionW = thumbsSize.x;
                resolutionH = "auto";
            }
        }

        var height = resolutionH !== "auto" ? resolutionH + "px" : resolutionH;
        var width  = resolutionW !== "auto" ? resolutionW + "px" : resolutionW;

        var text;

        if (context === 'chat') {
            var cText = info.cText || null;
            text = `<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="${info.msgIdx}" fileName="${fileName}">`+
                   `</a>${cText ? `&nbsp;<i class='fa finger fa-clipboard copyTextBtn' txt='${cText}'></i>` : ``}`;
        } else {
            text = mcService.myReplaceFormated(
                '<a id="link_#{idx}" href="" target="_blank"><img id="img_#{idx}" class="messageImg" src="#{loadImage}" dt="#{dt}" alt=""></a>',
                { idx: idx, loadImage: loadImage, dt: dt }
            );
        }

        if (onLoadImageNew) onLoadImageNew(imgInfo, info.msgIdx);
        else if (onLoadImage) onLoadImage(hash, info.msgIdx);

        return {
            html: text,
            notifyText: mcLang({ id: 570 , from: "common"}),
            flags: { loadImage: true, loadImageNew: imgInfo, hash: hash }
        };
    }

    function parseFileOld(msg, info) {
        var msgInfo  = msg.Msg.split(mcConst.CRLF);
        var hash     = msgInfo[0];
        var filePath = msgInfo[1];
        var fileDT   = msgInfo[2];
        var fileSize = msgInfo[3];
        var fileName = mcService.extractFileName(filePath);
        var href     = getLocalHostPath() + "/" + mcConst.pathAliases.AliasFiles + "/" + hash + "/" + encodeURI(fileName);
        var formatedSize = mcService.formatFileSize(fileSize);

        var text;

        if (context === 'chat') {
            var dTitle = mcLang({ id: 573 , from: "common"});
            var oTitle = mcLang({ id: 574 , from: "common"});

            text =
                '<i class="fa fa-file-o" aria-hidden="true"></i>' +
                `<a id="link_${hash}${info.msgIdx}" hash="${hash}" filepath="${filePath}" filedt="${fileDT}" filesize="${fileSize}" href="${href}" ` +
                `nick="${info.nick}" uin="${info.uin}" target="_blank" class="font120 fileLink openOrDownloadFile">${fileName}</a>&nbsp;&nbsp;` +
                `<i class="fa fa-download finger downloadFile" aria-hidden="true" title="${dTitle}"></i>&nbsp;` +
                `${isWeb ? `` : `<i class="fa fa-folder-open-o finger openInFolder" title="${oTitle}" aria-hidden="true"></i>&nbsp;`}` +
                `<span>(${formatedSize})</span>`;
        } else {
            text = mcService.myReplaceFormated(
                '<a id="link_#{hash}#{id}" hash="#{hash}" filepath="#{filepath}" filedt="#{filedt}" filesize="#{filesize}" href="#{href}" ' +
                'nick="#{nick}" uin="#{uin}" target="_blank" class="fileLink">#{file}</a>&nbsp;&nbsp;(#{formatedSize})</span>',
                {
                    id: info.msgIdx, nick: info.nick, uin: info.uin, hash: hash, href: href,
                    file: fileName, filepath: filePath, filedt: fileDT, filesize: fileSize, formatedSize: formatedSize
                }
            );
        }

        return {
            html: text,
            notifyText: mcLang({ id: 571, items: fileName , from: "common"}),
            flags: {}
        };
    }

    function parseFileNew(msg, info) {
        var fileInfo = mcService.StringToObj(msg.Msg.split(mcConst.CRLF)[0]);
        var hash     = fileInfo.Hash;

        if (!hash) {
            return {
                html: "<b><i>Can't parse message:</i></b><span class='messageText'>" + msg.Msg + "</span>",
                notifyText: "",
                flags: {}
            };
        }

        var filePath = fileInfo.FileName;
        var fileDT   = fileInfo.UTCWriteTime;
        var fileSize = fileInfo.Size;
        var fileName = mcService.extractFileName(filePath);
        var href     = getLocalHostPath() + "/" + mcConst.pathAliases.AliasFiles + "/" + hash + "/" + encodeURI(fileName);
        var formatedSize = mcService.formatFileSize(fileSize);

        var text;

        if (context === 'chat') {
            var dTitle  = mcLang({ id: 573 , from: "common"});
            var oTitle  = mcLang({ id: 574 , from: "common"});
            var isMedia = mcService.inArrayB(mcService.extractFileExtension(filePath), ['mpg', 'mp4', 'avi', 'mkv', 'webm', 'mov', 'aac']);

            text =
                '<i class="fa fa-file-o" aria-hidden="true"></i>' +
                `<a id="link_${hash}${info.msgIdx}" hash="${hash}" filepath="${filePath}" filedt="${fileDT}" filesize="${fileSize}" href="${href}" ` +
                `nick="${info.nick}" uin="${info.uin}" target="_blank" class="font120 fileLink openOrDownloadFile">${fileName}</a>&nbsp;&nbsp;` +
                `<i class="fa fa-download finger downloadFile" aria-hidden="true" title="${dTitle}"></i>&nbsp;` +
                `${isWeb ? `` : `<i class="fa fa-folder-open-o finger openInFolder" title="${oTitle}" aria-hidden="true"></i>&nbsp;`}` +
                (isMedia ? `<i class="fa fa-play-circle-o finger playMediaFile brown bolder font120" aria-hidden="true" title="Play media" link="${href}"></i>&nbsp;` : ``) +
                `<span>(${formatedSize})</span>`;
        } else {
            text = mcService.myReplaceFormated(
                '<a id="link_#{hash}#{id}" hash="#{hash}" filepath="#{filepath}" filedt="#{filedt}" filesize="#{filesize}" href="#{href}" ' +
                'nick="#{nick}" uin="#{uin}" target="_blank" class="fileLink">#{file}</a>&nbsp;&nbsp;(#{formatedSize})</span>',
                {
                    id: info.msgIdx, nick: info.nick, uin: info.uin, hash: hash, href: href,
                    file: fileName, filepath: filePath, filedt: fileDT, filesize: fileSize, formatedSize: formatedSize
                }
            );
        }

        return {
            html: text,
            notifyText: mcLang({ id: 571, items: fileName , from: "common"}),
            flags: {}
        };
    }

    function parseReply(msg, info) {
        var replyMsg    = mcService.StringToObj(msg.Msg);
        var originalMsg = mcService.StringToObj(replyMsg.Original);

        if (!originalMsg) {
            return {
                html: "<b><i>Can't parse message:</i></b> " + msg.Msg,
                notifyText: "",
                flags: {}
            };
        }

        originalMsg.time = originalMsg.dtUTC ? mcService.utcTime(originalMsg.dtUTC).split('.') : "";

        var origText = "";
        var origNick = "";

        switch (originalMsg.MsgType) {
            case mcConst._CMD_.msgType.TEXT:
                origText = originalMsg.Msg.split(mcConst.CRLF);
                for (var j = 0; j < origText.length; j++) origText[j] = clearText(origText[j]);
                origText = origText.join("<br>");
                break;

            case mcConst._CMD_.msgType.FILE_NEW:
                var fInfo = mcService.StringToObj(originalMsg.Msg);
                origText = fInfo && fInfo.FileName ? mcService.extractFileName(fInfo.FileName) : originalMsg.Msg;
                break;

            case mcConst._CMD_.msgType.IMAGE_NEW:
            case mcConst._CMD_.msgType.EXCEL_NEW:
                origText = mcLang({ id: 570 , from: "common"}); // зображення
                break;

            default:
                if (mcService.isString(originalMsg.Msg)) {
                    origText = originalMsg.Msg.split(mcConst.CRLF);
                    for (var k = 0; k < origText.length; k++) origText[k] = clearText(origText[k]);
                    origText = origText.join("<br>");
                } else {
                    origText = "unknown reply type: " + originalMsg.MsgType;
                }
        }

        if (originalMsg.time === '') {
            origNick = getDisplayName(originalMsg.UIN);
        } else {
            origNick = getDisplayName(originalMsg.UIN);
        }

        var origTimeStr = "";
        if (originalMsg.time && originalMsg.time.length >= 6) {
            origTimeStr = originalMsg.time[3] + ":" + originalMsg.time[4] + ":" + originalMsg.time[5];
        }

        var replyText = replyMsg.Reply.Msg.split(mcConst.CRLF);
        for (var i = 0; i < replyText.length; i++) replyText[i] = clearText(replyText[i]);
        replyText = replyText.join('<br/>');

        if (context === 'chat') {
            replyText = mcService.replaceMcComponents(replyText, isWeb, null);
            replyText = mcService.ReplaceURLs(replyText, { callback: urlCallback });
        } else {
            replyText = mcService.ReplaceURLs(replyText, { callback: urlCallback });
        }

        replyText = parseEmoji(replyText);

        var quoteHtml = context === 'chat'
            ? `<div class='quoteMessage'><div class="grayBorderTop"><div class="messageUserName" uin="${originalMsg.UIN}"><b><i>${origNick}</i></b> ${origTimeStr}</div></div>${origText}</div>`
            : mcService.insertIco("fa-quote-right", "gray") +
              `<b><i>${origNick}</i></b>  ${origTimeStr}<br>` +
              "&nbsp;&nbsp;<b>|</b> " + origText.split("<br>").join("<br>&nbsp;&nbsp;<b>| </b>") + "<br><br>";

        return {
            html: quoteHtml + replyText,
            notifyText: replyText,
            flags: {}
        };
    }

    function parseBarcode(msg, info) {
        var barData = mcService.StringToObj(msg.Msg.split(mcConst.CRLF)[0]);

        if (!mcService.isObject(barData) || !barData || !barData.Data) {
            return {
                html: "<br>---- Incorrect Barcode Data ----<br><br>",
                notifyText: "",
                flags: {}
            };
        }

        var type = barData.Format.replace(/[_|-]/g, "").toLowerCase();
        var text = "";
        var onAdd = null;

        if (type === "qrcode") {
            text = "<span id='qrcode_id_" + info.msgIdx + "'></span>";
            onAdd = function (){
                if (typeof QRCode !== 'undefined') {
                    setTimeout(function(){
                        new QRCode(document.getElementById("qrcode_id_" + info.msgIdx), {
                            text: barData.Data, width: 200, height: 200,
                            colorDark: "#131313", colorLight: "#ffffff",
                            correctLevel: QRCode.CorrectLevel.L
                        });
                    }, 700);
                }
            };
        } else {
            if ((type === "upca" || type === "ean13" || type === "ean8") && typeof webix !== 'undefined') {
                if (type === "upca") type = "upcA";
                var barcode = webix.ui({ view: "barcode", type: type, value: barData.Data });
                var barNode = barcode.getNode();
                text = barNode.outerHTML.replace("<canvas ", "<img src=\"" + barNode.firstChild.toDataURL() + "\" ").replace("canvas>", "img>");
                barcode.destructor();
            } else if (barData.Body) {
                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 {
                text = "<br>---- Incorrect Barcode Data ----<br><br>";
            }
        }

        return {
            html: text,
            notifyText: "",
            flags: { onAdd: onAdd }
        };
    }

    function parseCall(msg, info) {
        var callInfo    = mcService.StringToObj(msg.Msg);
        var isMyCall    = callInfo.UINFrom == getCurrentUIN();
        var time        = callInfo.Duration < 0 ? 0 : callInfo.Duration;
        var displayName = info.nick;
        var text        = "";

        switch (callInfo.State){
            case listCE.server_state_cancelled:
                text = isMyCall
                    ? mcLang({ id: 495, items: mcService.MyFormatTime(time, time < 3600) , from: "common"})
                    : (callInfo.Duration >= 0 ? mcLang({ id: 494, items: [displayName, mcService.MyFormatTime(time, time < 3600)] , from: "common"}) : mcLang({ id: 500 , from: "common"}));
                break;
            case listCE.server_state_rejected:
                text = isMyCall ? mcLang({ id: 496 , from: "common"}) : mcLang({ id: 500 , from: "common"});
                break;
            case listCE.server_state_closed:
                text = isMyCall
                    ? mcLang({ id: 495, items: mcService.MyFormatTime(time, time < 3600) , from: "common"})
                    : mcLang({ id: 494, items: [displayName, mcService.MyFormatTime(time, time < 3600)] , from: "common"});
                break;
            case listCE.server_state_error:
                text = mcService.Error(callInfo.ErrorNum, callInfo.ErrorDesc);
                break;
            case listCE.server_state_busy:
                text = mcLang({ id: 501 , from: "common"});
                break;
            case listCE.server_state_rejected_os_too_old:
                text = isMyCall ? mcLang({ id: 673 , from: "common"}) : mcLang({ id: 674 , from: "common"});
                break;
            case listCE.server_state_cancelled_by_timeout:
                text = isMyCall ? mcLang({ id: 497 , from: "common"}) : mcLang({ id: 672 , from: "common"});
                break;
            case listCE.outgoing_call_remote_close:
                text = mcLang({ id: 495, items: mcService.MyFormatTime(time, time < 3600) , from: "common"});
                break;
            case listCE.outgoing_call_rejected_user_not_in_contacts:
                text = isMyCall ? mcLang({ id: 728 , from: "common"}) : mcLang({ id: 727 , from: "common"});
                break;
            case listCE.incoming_call_rejected:
                text = mcLang({ id: 506 , from: "common"});
                break;
            default:
                text = "Call from: " +
                        getDisplayName(callInfo.UINFrom) + " (UIN:" + callInfo.UINFrom + ") -> to: " +
                        getDisplayName(callInfo.UINTo)   + " (UIN:" + callInfo.UINTo + ")" +
                        " Duration: " + mcService.MyFormatTime(time, time < 3600);
        }

        var _icon = mcService.insertIco("fa-" + (callInfo.Type == 0 ? "phone" : callInfo.Type == 1 ? "video-camera" : "desktop"));
        text = '<span class="messageText brown">' + _icon + text + '</span>';

        return { html: text, notifyText: text, flags: {} };
    }

    function parseKanban(msg, info) {
        var kMSG = mcService.StringToObj(msg.Msg);
        var text = "";

        function _parseLink(source) {
            return source ? mcService.ReplaceURLs(source, { callback: urlCallback }) : "";
        }

        switch (kMSG.What){
            case mcConst.KanbanEvents.TASK_ADDED:
            case mcConst.KanbanEvents.TASK_PERFORMER_CHANGED:
                text = mcLang({ id: 1988, items: [
                    mcService.createKanbanLink(kMSG.ProjectID, kMSG.TaskID),
                    kMSG.DisplayName, _parseLink(kMSG.TaskName), _parseLink(kMSG.Description)
                ] , from: "common"});
                break;
            case mcConst.KanbanEvents.COMMENT_ADDED:
                text = mcLang({ id: 1989, items: [
                    mcService.createKanbanLink(kMSG.ProjectID, kMSG.TaskID, kMSG.CommentID),
                    kMSG.DisplayName, _parseLink(kMSG.TaskName), _parseLink(kMSG.Comment)
                ] , from: "common"});
                break;
            case mcConst.KanbanEvents.TASK_MOVED:
                text = mcLang({ id: 1990, items: [
                    mcService.createKanbanLink(kMSG.ProjectID, kMSG.TaskID),
                    kMSG.DisplayName,
                    _parseLink(kMSG.TaskName),
                    kMSG.OldStageName + " &gt; " + kMSG.StageName
                ] , from: "common"});
                break;
            case mcConst.KanbanEvents.TASK_CHANGED:
                text = mcLang({ id: 1991, items: [
                    mcService.createKanbanLink(kMSG.ProjectID, kMSG.TaskID),
                    kMSG.DisplayName, _parseLink(kMSG.TaskName), _parseLink(kMSG.Description)
                ] , from: "common"});
                break;
            case mcConst.KanbanEvents.TASK_DELETED:
                text = mcLang({ id: 1992, items: [
                    mcService.createKanbanLink(kMSG.ProjectID, kMSG.TaskID),
                    kMSG.DisplayName, _parseLink(kMSG.TaskName), _parseLink(kMSG.Description),
                    kMSG.ProjectName
                ] , from: "common"});
                break;
            case mcConst.KanbanEvents.PROJECT_CLOSED:
                text = mcLang({ id: 1993, items: [
                    mcService.createKanbanLink(kMSG.ProjectID, kMSG.TaskID),
                    kMSG.DisplayName, kMSG.ProjectName
                ] , from: "common"});
                break;
            default:
                text = msg.Msg;
        }

        return { html: text, notifyText: "", flags: {} };
    }

    function parseForum(msg, info) {
        var forumMsg = mcService.StringToObj(msg.Msg);

        var text = mcService.myReplaceFormated(
            "<div>#{msgType}</div><div>#{topic}</div><a href='#{link}' target='_blank'>#{linkText}</a>",
            {
                msgType: forumMsg.What === mcConst.ForumNotify.POST_CREATED
                    ? mcLang({ id: 1994 , from: "common"}) + forumMsg.DisplayName
                    : mcLang({ id: 1995 , from: "common"}) + forumMsg.DisplayName,
                topic: mcLang({ id: 1996 , from: "common"}) + forumMsg.Topic,
                linkText: mcLang({ id: 1997 , from: "common"}),
                link: "/forum/#" + forumMsg.Path
            }
        );

        return { html: text, notifyText: "", flags: {} };
    }

    function parseCode(msg, info) {
        if (!msg.Msg) return { html: "", notifyText: "", flags: {} };

        var codeData = mcService.StringToObj(msg.Msg);
        var text = "<pre class='code_snippet'>" + (codeData.Msg || msg.Msg).replace(/</ig, "&lt;") + "</pre>";

        return {
            html: text,
            notifyText: "",
            flags: { codeSnippet: true }
        };
    }

    function parseMultilang(msg, info) {
        var textArr  = msg.Msg.split(mcConst.CRLF);
        var faultTxt = textArr[0];
        var mlObj    = mcService.StringToObj(textArr[0]);
        var text;

        if (mlObj[mcConst.Lang.toUpperCase()]){
            text = clearText(mlObj[mcConst.Lang.toUpperCase()]);
        } else if (mlObj.Default && mlObj[mlObj.Default]){
            text = clearText(mlObj[mlObj.Default]);
        } else {
            text = clearText(mlObj.EN || "Incorrect Multilang message: " + faultTxt);
        }

        var notifyText = text;

        text = mcService.replaceMcComponents(text, isWeb, null);

        if (context === 'chat') {
            var total = { twitter: false, twId: null };
            text = mcService.ReplaceURLs(text, { callback: function(_text, _link){
                return urlCallbackChat(_text, _link, msg.Idx, total);
            }});
        } else {
            text = mcService.ReplaceURLs(text, { callback: urlCallback });
        }

        text = parseEmoji(text);

        return {
            html: text,
            notifyText: notifyText,
            flags: context === 'chat' ? { twitter: total.twitter, twId: total.twId } : {}
        };
    }

    function parseFormattedText(msg, info) {
        // [p]:
        // indent
        // color
        // =left
        //
        // [font]
        // bg
        // color
        // style
        // size
        // style=bius
        // size [0..6]
        // indent +10
        // split(new MyRegExp(/(\[.*?\])|(\w+)/gi))

        var txt = [].concat(msg.Msg.split(new MyRegExp(/(\[.*?\])|(\w+\s*)/gi)));
        var hasAlign = false;

        var res = txt.reduce(function (accumulator, current, id) {
            function _parse(_accumulator, _current) {
                if (_current[0] === "[" && _current[_current.length - 1] === "]") {
                    var tmp;
                    if (/^\[\/.+]$/.test(_current)){
                        tmp = _current.replace(/\[|]|\//g, "");
                        _current = { close: true, tag: tmp.replace("font", "span") };
                    } else {
                        _current = [].concat(_current.replace(/\[|\]/g, "").split(new MyRegExp(/\s+/)))
                            .reduce(function (_acc, _cur, _id) {
                                if (!_acc){ _acc = {}; }
                                else if (mcService.isString(_acc)){
                                    tmp = _acc.split("=");
                                    _acc = { tag: tmp[0].replace("font", "span"), params: tmp[1] || null };
                                }
                                tmp = _cur.split("=");
                                _acc[tmp[0]] = tmp[1] || null;
                                return _acc;
                            });

                        if (mcService.isString(_current) && _current.indexOf('=') > 0){
                            tmp = _current.split("=");
                            _current = { tag: tmp[0].replace("font", "span"), params: tmp[1] || null };
                        }
                    }
                    _accumulator.push(_current);
                } else {
                    _accumulator.push(_current.replace(/</g, "&lt;").replace(/\n/g, "<br>"));
                }
            }

            if (mcService.isString(accumulator)){
                var tmp2 = accumulator;
                accumulator = [];
                _parse(accumulator, tmp2);
            }
            _parse(accumulator, current);
            return accumulator;
        });

        var p_not_closed = false;

        if (mcService.isArray(res)){
            res = res.map(function (item) {
                var result = "";
                if (mcService.isObject(item)){
                    var tag   = item.tag;
                    var style = [];

                    if (item.close) return `</${tag}>`;

                    delete item.tag;

                    Object.keys(item).forEach(function (_param) {
                        switch (_param) {
                            case "params":
                                if (tag === "p"){ style.push(`text-align: ${item[_param]};`); hasAlign = true; }
                                break;
                            case "indent": style.push(`text-indent: ${item[_param]}pt;`); break;
                            case "color":  style.push(`color: ${item[_param]};`); break;
                            case "bg":     style.push(`background-color: ${item[_param]};`); break;
                            case "style":
                                var list = item[_param].split('');
                                list.forEach(function (ii) {
                                    switch (ii) {
                                        case "b": style.push(`font-weight: bolder;`); break;
                                        case "i": style.push(`font-style: italic;`); break;
                                        case "u": style.push(`text-decoration: underline;`); break;
                                        case "s": style.push(`text-decoration: line-through;`); break;
                                    }
                                });
                                break;
                            case "size": style.push(`font-size: ${12 + 2 * item[_param]}px;`); break;
                        }
                    });

                    if (tag === "p" && p_not_closed){ result += `</p>`; p_not_closed = false; }
                    if (tag === "p") p_not_closed = true;

                    result += `<${tag} style="${style.join(' ')}">`;
                } else if (mcService.isString(item)) {
                    result += item;
                }
                return result;
            });

            var text = res.join('');
            if (p_not_closed) text += `</p>`;

            return { html: text, notifyText: "", flags: { hasAlign: hasAlign } };
        }

        return { html: res || "", notifyText: "", flags: { hasAlign: hasAlign } };
    }

    function parseTable(msg, info) {
        /*
         "Rows" : 9,
         "Cols" : 5,
         "Width" : "100%",
         "Color" : "gray",
         "BorderColor" : "black",
         "CellBorderColor" : "blue",
         "BorderWidth" : 1,
         "CellBorderWidth" : 1,
         "CellVAlign" : "middle",
         "CellHAlign" : "right",
         "Cells" : [
             {
                 "Row" : 0,
                 "Col" : 0,
                 "ColSpan" : 5,
                 "Color" : "green",
                 "Borders" : "bottom",
                 "Text" : "[p=center][font size=35 style=b]September data[/font]"
             },
             {
                 "Row" : 1,
                 "Col" : 0,
                 "Text" : "bla-bla-bla (0,0)",
                 "Color" : "#FF0000"
             },
             {
                 "Row" : 2,
                 "Col" : 0,
                 "Text" : "[p=center][font size=15 color=#FFFFFF bg=#FF0000 style=bius]Colored Text[/font]",
                 "Borders" : "none"
             },
         ]
         */

        var struct = mcService.StringToObj(msg.Msg);
        var table = [];

        for (var i = 0; i < struct.Rows; i++){
            table.push(new Array(struct.Cols || 1).fill("<td>&nbsp;</td>"));
        }

        if (struct.Cells && struct.Cells.length && mcService.isArray(struct.Cells)) {
            struct.Cells.forEach(function (item) {
                var parsedCell = parseFormattedText({Msg: item.Text}, info);
                item.Width = item.Width ? item.Width + "px" : "";

                table[item.Row][item.Col] = `<td`+
                    `${item.ColSpan ? " colspan=" + item.ColSpan : ""}`+
                    `${item.RowSpan ? " rowspan=" + item.RowSpan : ""}`+
                    ` style="`+
                         `background-color: ${item.Color || "transparent"}; `+
                         `width: ${item.Width || "fit-content"};`+
                    `"`+
                    `>${parsedCell.html || "&nbsp;"}</td>`;

                if (item.ColSpan || item.RowSpan) {
                    for (var _rowID = item.Row; _rowID <= (item.Row + (item.RowSpan || 0)); _rowID ++){
                        for (var _colID = item.Col; _colID < (item.Col + (item.ColSpan || 0)); _colID ++){
                            if (table[_rowID][_colID] === "<td>&nbsp;</td>") table[_rowID][_colID] = "";
                        }
                    }
                }
            });

            table = table.map(function (_row, id) {
                return `<tr>${table[id].join("")}</tr>`;
            });
        }

        var text = `<table class="intextTable noShadow"` +
             ` style="` +
                 `width:${struct.Width || "100%"}; ` +
                 `background-color:${struct.Color || "#fff"}; ` +
                 `vertical-align:${struct.CellVAlign || "middle"}; ` +
                 `text-align:${struct.CellHAlign || "left"}; ` +
                 `border-color:${struct.CellBorderColor || "black"}; ` +
        `">${table.join("")}</table>`;

        return { html: text, notifyText: "", flags: { isTable: true } };
    }

    function parseAction(msg, info) {
        var actMsg = mcService.StringToObj(msg.Msg);
        var text = msg.Msg;

        if (actMsg && actMsg.ActionIdx){
            switch (actMsg.ActionIdx){
                case 19:
                    text = `<a href='#' action='${actMsg.ActionIdx}' class='_openAction'>${mcService.insertIco("fa-bolt")} ${mcLang({ id: 767 , from: "common"})}</a>`;
                    break;
                default:
                    text = msg.Msg;
            }
        }

        return { html: text, notifyText: "", flags: {} };
    }

    function parseComment(msg, info) {
        var text = (msg.Text || msg.Msg).split(mcConst.CRLF);

        for (var i = 0; i < text.length; i++){
            text[i] = text[i].replace(/</ig, "&lt;");
        }

        text = mcService.ReplaceURLs(text.join('<br/>'), { callback: urlCallback });

        return { html: text, notifyText: "", flags: {} };
    }

    function parseRedirect(msg, info) {
        var rdData = mcService.StringToObj(msg.Msg);
        var text;

        if (context === 'chat') {
            if (rdData.UIN == getCurrentUIN()) {
                text = mcLang({ id: 615, items: [rdData.DisplayNameTo, rdData.DisplayNameFrom] , from: "common"});
            } else if (rdData.UINFrom == getCurrentUIN()) {
                text = mcLang({ id: 616, items: [rdData.DisplayName, rdData.DisplayNameTo] , from: "common"});
            } else if (rdData.UINTo == getCurrentUIN()) {
                text = mcLang({ id: 617, items: [rdData.DisplayName, rdData.DisplayNameFrom] , from: "common"});
            } else {
                text = mcLang({ id: 2049, items: [rdData.DisplayName, rdData.DisplayNameFrom, rdData.DisplayNameTo] , from: "common"});
            }
        } else {
            text = mcLang({ id: 2049, items: [rdData.DisplayName, rdData.DisplayNameFrom, rdData.DisplayNameTo] , from: "common"});
        }

        return { html: text, notifyText: "", flags: {} };
    }

    function parseConfCreate(msg, info) {
        var confInfo = mcService.StringToObj(msg.Msg);
        var text;

        if (confInfo.JoinUsers) {
            var uList = confInfo.JoinUsers.split(",");
            text = mcLang({ id: 2242, items: [confInfo.Name, confInfo.Topic, uList.length, confInfo.JoinUsers] , from: "common"});
        } else {
            text = mcLang({ id: 2243, items: [confInfo.Name, confInfo.Topic] , from: "common"});
        }

        return { html: text, notifyText: "", flags: {} };
    }

    function parseBroadcast(msg, info) {
        // "UIN":17614,
        // "TotalCount":1,
        // "ChangeID":61,
        // "ID":962,
        // "MsgBrief":"rtertert",
        // "ReadCount":1
        var bInfo = mcService.StringToObj(msg.Msg);
        var name  = getDisplayName(bInfo.UIN);

        if (name === "noNameUser") {
            name = "<i id='broadcast-user-uin-" + bInfo.UIN + "'>load name</i>";
        } else {
            name = name + " (UIN: " + bInfo.UIN + ")";
        }

        var text = mcLang({ id: 643, items: [bInfo.ID, bInfo.TotalCount, bInfo.ReadCount, name] , from: "common"});

        return { html: text, notifyText: "", flags: {} };
    }

    function parseAudioVideo(msg, info) {
        var avInfo   = mcService.StringToObj(msg.Msg);
        var filePath = avInfo.FileName;
        var fileName = mcService.extractFileName(filePath);
        var href     = getLocalHostPath() + "/" + mcConst.pathAliases.AliasFiles + "/" + avInfo.Hash + "/" + encodeURI(fileName);
        var text;

        if (msg.MsgType === mcConst._CMD_.msgType.AUDIO){
            text = `<audio id="link_${avInfo.Hash}${info.msgIdx}" controls="controls" hash="${avInfo.Hash}" src="${href}" ` +
                   `nick="${info.nick}" uin="${info.uin}" class="font120" ` +
                   `style="max-width:100%; max-height: 400px; height: 50px; width: 400px"></audio>`;
        } else {
            text = `<video id="link_${avInfo.Hash}${info.msgIdx}" controls="controls" hash="${avInfo.Hash}" src="${href}" ` +
                   `nick="${info.nick}" uin="${info.uin}" class="font120" ` +
                   `style="max-width:100%; max-height: 400px; height: auto"></video>`;
        }

        return { html: text, notifyText: mcLang({ id: 571, items: fileName , from: "common"}), flags: {} };
    }

    function parseFtpPublic(msg, info) {
        var text = "<a href='" + mcConst.ftpPublic + "' class='__ftpFolder__'>" + mcLang({ id: 652 , from: "common"}) + "</a>";
        return { html: text, notifyText: "", flags: {} };
    }

    function parseFtpPersonal(msg, info) {
        var text = "<a href='" + mcConst.ftpPersonal + info.uin + "' class='__ftpFolder__'>" + mcLang({ id: 653 , from: "common"}) + "</a>";
        return { html: text, notifyText: "", flags: {} };
    }

    function parseMychatGuest(msg, info) {
        var guestInfo = mcService.StringToObj(msg.Msg);
        var text = mcLang({ id: 732 , from: "common"}) + mcService.ReplaceURLs(guestInfo.Link, { callback: function(_text, _link){
            var desc = "";
            try { desc = decodeURI(_link || _text); } catch (e) { desc = _link || _text; }
            return '<a href="' + _link + '" title="' + _link + '" target="_blank">' + desc + '</a>';
        }});
        return { html: text, notifyText: "", flags: {} };
    }

    // -----------------------------------------------
    // Реакції
    // -----------------------------------------------

    this.renderReactionBlock = function(reactions) {
        // "Idx"  : 2301, // уникальный идентификатор оригинального сообщения
        // "Reactions" : { // объект с реакциями, его нужно будет переписать в базу данных для месаджа с указанным индексом
        // // likes-23,dislikes-3,smileys-1
        // "Total" : ["1F44D-23","1F44E-3,263A-1"],
        //     // список реакций, набор вложенных объектов, ключ - UIN юзера, значение - Who:What-Where
        //     // если реакция удаляется - то запись из блока "Details" тоже удаляется.
        //     // если реакция обновляется - то обновляется сама реакция и дата/время в UTC
        //     "Details" : {
        //     "16" : "1F44D-07.10.2024.17.25.13",
        //     "11" : "1F44E-07.10.2024.18.00.17"
        // ...
        // }
        var res       = "";
        var _exclude  = [];
        var smiles    = [
            "1F642", "1F44D", "1F44E", "2764", "1F606", "1F62F",
            "1F622", "1F631", "1F621", "1F4A9", "1F440", "2705",
            "274C", "1F4AF", "270D", "1F37A"
        ];
        var actionClass = "emjClick";

        if (context === 'chat' && reactions && reactions.Total && reactions.Total.length) {
            var avatars   = {};
            var titleInfo = [];

            Object.keys(reactions.Details).forEach(function (_uin) {
                var avaID = reactions.Details[_uin].split("-")[0];
                var name = getDisplayName(_uin);

                if (!avatars[avaID]) avatars[avaID] = [];
                avatars[avaID].push([_uin, name]);
                titleInfo.push(name);
            });

            res = '<div class="emjMainBlock emjPlace">';

            reactions.Total.forEach(function (emj) {
                var info     = emj.split('-');
                var emjCode  = info[0];
                var emjCount = info[1];
                var smallAva = "";

                _exclude.push(info[0]);

                for (var i=0; i < (emjCount <= 3 ? emjCount : 3); i++){
                    var _uin     = avatars[emjCode][i][0];
                    var _name    = avatars[emjCode][i][1];
                    var _isWebSup= _name.indexOf("(websup)") === 0;
                    var _icon    = _isWebSup ? mcService.getTwoLettersName(_name.replace("(websup)", "")) : mcService.getTwoLettersName(_name);

                    smallAva += `<span class="circleAvatarSmall shadow2px boxShadow smallChatAvatar${_uin}">${_icon}</span>`;
                }

                res += `<span class="emjBlock" title="${titleInfo.join(', ')}">` +
                    mcService.insertSmileyImg(emjCode, actionClass) + (emjCount > 3 ? `<span class="emjCount">${emjCount}</span>` : smallAva ) +
                    `</span>`;
            });

            var _getList = function (exclude) {
                return (smiles.map(function (itm) {
                    return (exclude || []).indexOf(itm) === -1 ? mcService.insertSmileyImg(itm, actionClass) : "";
                })).join('');
            };

            res += `<span class="emjPlusWrap"><span class="emjPlusBtn">+</span></span>` +
                   `<span class="emjMore">${_getList(_exclude)}</span></div>`;
        } else

        if (context === 'chat') {
            var _getList2 = function (exclude) {
                return (smiles.map(function (itm) {
                    return (exclude || []).indexOf(itm) === -1 ? mcService.insertSmileyImg(itm, actionClass) : "";
                })).join('');
            };

            res = `<span class="emjFirstSelect emjPlace">` +
                    mcService.insertSmileyImg("1F642", actionClass) +
                    `<span class="emjAllSmiles">${_getList2(["1F642"])}</span>` +
                  `</span>`;
        } else

        if (context === 'admin' && reactions && reactions.Total) {
            res = '<div class="emjMainBlock emjPlace">';

            reactions.Total.forEach(function (emj) {
                var info = emj.split('-');
                res += "<span class='emjBlock'>" + mcService.insertSmileyImg(info[0], actionClass) +
                       (info[1] > 0 ? `<span class="emjCount">${info[1]}</span>` : "") + "</span>";
            });

            res += `<span class="emjPlusWrap"></span></div>`;
        }

        return res;
    };

    // -----------------------------------------------
    // Головний маршрутизатор
    // -----------------------------------------------

    this.renderMessage = function(msg, extraInfo) {
        var info = extraInfo || {};
        info.msgIdx = info.msgIdx || msg.Idx || msg.MsgIdx;
        info.nick   = info.nick   || msg.Nick || getDisplayName(msg.UINFrom);
        info.uin    = info.uin    || msg.UINFrom;

        switch (msg.MsgType){
            case mcConst._CMD_.msgType.OLD_SHIT:
            case mcConst._CMD_.msgType.INTEGRATION_API:
            case mcConst._CMD_.msgType.TEXT:
                return parseText(msg, info);

            case mcConst._CMD_.msgType.LAN_LINK:
            case mcConst._CMD_.msgType.ANY_LINK:
                return parseLink(msg, info);

            case mcConst._CMD_.msgType.IMAGE:
            case mcConst._CMD_.msgType.CLIPBOARD_EXCEL:
                return parseImageOld(msg, info);

            case mcConst._CMD_.msgType.EXCEL_NEW:
            case mcConst._CMD_.msgType.IMAGE_NEW:
                return parseImageNew(msg, info);

            case mcConst._CMD_.msgType.FILE:
                return parseFileOld(msg, info);

            case mcConst._CMD_.msgType.FILE_NEW:
                return parseFileNew(msg, info);

            case mcConst._CMD_.msgType.REPLY:
                return parseReply(msg, info);

            case mcConst._CMD_.msgType.BARCODE:
                return parseBarcode(msg, info);

            case mcConst._CMD_.msgType.MEDIA_CALL:
                return parseCall(msg, info);

            case mcConst._CMD_.msgType.KANBAN_NOTIFY:
                return parseKanban(msg, info);

            case mcConst._CMD_.msgType.FORUM_NOTIFY:
                return parseForum(msg, info);

            case mcConst._CMD_.msgType.SOURCE_CODE:
                return parseCode(msg, info);

            case mcConst._CMD_.msgType.MULTILANG:
                return parseMultilang(msg, info);

            case mcConst._CMD_.msgType.FORMATTED_TEXT:
            case mcConst._CMD_.msgType.FORWARD:
                return parseFormattedText(msg, info);

            case mcConst._CMD_.msgType.TABLE:
                return parseTable(msg, info);

            case mcConst._CMD_.msgType.ACTION:
                return parseAction(msg, info);

            case mcConst._CMD_.msgType.COMMENT:
                return parseComment(msg, info);

            case mcConst._CMD_.msgType.PRIVATE_REDIRECT:
                return parseRedirect(msg, info);

            case mcConst._CMD_.msgType.CONF_JUST_CREATED:
                return parseConfCreate(msg, info);

            case mcConst._CMD_.msgType.BROADCAST_NOTIFY:
                return parseBroadcast(msg, info);

            case mcConst._CMD_.msgType.AUDIO:
            case mcConst._CMD_.msgType.VIDEO:
                return parseAudioVideo(msg, info);

            case mcConst._CMD_.msgType.FTP_PUBLIC:
                return parseFtpPublic(msg, info);

            case mcConst._CMD_.msgType.FTP_PERSONAL:
                return parseFtpPersonal(msg, info);

            case mcConst._CMD_.msgType.MYCHAT_GUEST:
                return parseMychatGuest(msg, info);

            case mcConst._CMD_.msgType.TEXT_MODIFY:
            case mcConst._CMD_.msgType.REACTION:
                return null;

            default:
                return {
                    html: "Tech message: " + (msg.Msg || ""),
                    notifyText: "",
                    flags: {}
                };
        }
    };

    // -----------------------------------------------
    // Публічні утиліти
    // -----------------------------------------------

    this.clearText  = clearText;
    this.parseEmoji = parseEmoji;
}



function mcEmojiList(storage) {
    // Якщо передано $rootScope замість storage-адаптера, створити обгортку
    if (storage && storage.Storage && storage.Storage.popularSmiles) {
        var _rs = storage;
        storage = {
            save: function(data) {
                _rs.Storage.popularSmiles.save(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.POPULARSMILES, data);
            },
            load: function(cb) {
                _rs.Storage.popularSmiles.load(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN, mcConst.storageOpts.POPULARSMILES, cb);
            }
        };
    }

    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 (storage && !saveTimer){
            saveTimer = setTimeout(function (){
                saveTimer = null;

                storage.save(JSON.stringify(popularSmileys));
            }, 2000);
        }
    };

    this.load = function (){
        if (storage) {
            storage.load(function (data) {
                if (data) popularSmileys = mcService.StringToObj(data);
            });
        }
    };

}

﻿function mcCmdParser (mcConnect, $rootScope, mcPlaySound, mcStatistics){
    "use strict";

    var sendCMDToServer = {}; // команди від клієнда до сервера
    var ProcessCMD      = {}; // вхідні команди із сервера
    var pCmd            = {}; // відповідність деяких команд сервера і клієнта для обробки відповіді на запит зі сторони клієнта
    var eCmd            = {}; // команди взаємодії з electronjs
    var lastError       = 0;
    var callBackPool    = {};
    var _CMD_           = mcConst._CMD_;
    var electronHandler = null;
    var parseCmdPool    = [];
    var cmdInProgress   = false;
    var traffic_view    = mcConst.traffic_type.off;
    var tryRegister     = false;
    var indexCMD        = 1;

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

    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(window._messages_.dialogsList.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_media_conf_user_list]           = _CMD_.cs_get_media_conf_user_list;
    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' + $rootScope.chatAliases.Login, []);
        } else {
            $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{}]);
        }

        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 (($rootScope.ErrorActions[err.ErrNum] && $rootScope.ErrorActions[err.ErrNum](err.Params)) || criticalError(err.ErrNum)){
            $rootScope.$broadcast('StopPingTimer', []);
        }

        if (!mcConst.LoggedIn && $rootScope.loginFormHidden) {
            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' + $rootScope.chatAliases.Login, []);
                        } else {
                            location.reload(); 
                        }
                    }]);
                }
            }
        } else

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

        if (err.ErrNum !== mcConst._CMD_.Errors.NetworkError ||
            (!$rootScope.loginFormHidden && !tryRegister && (  // login for is showed now  AND not try register
                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' + $rootScope.chatAliases.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

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

    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' + $rootScope.chatAliases.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' + $rootScope.chatAliases.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' + $rootScope.chatAliases.Login, []);
            }
        } else

        if (mcConst.LoggedIn || mcConst.PingTimer) {
            $rootScope.$broadcast('hide' + $rootScope.chatAliases.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' ,[]);
                $rootScope.$broadcast(window._messages_.callManager.stopCurrentCall);
                $rootScope.$broadcast(window._messages_.dialogsCtrl.clearNotificationBlink);

                __reconnect(true);
            }
        } else

        if (!$rootScope.isWebClient && ($rootScope.lastError == _CMD_.Errors.Disconnected || $rootScope.lastError == _CMD_.Errors.Disconnected)) {
            __reconnect();

            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [mcConst._CMD_.ce_client_disconnected]);
            $rootScope.$broadcast(window._messages_.callManager.stopCurrentCall);
            $rootScope.$broadcast(window._messages_.dialogsCtrl.clearNotificationBlink);
        }
    };

    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' + $rootScope.chatAliases.ChatWrapper, []);
            $rootScope.$broadcast('show' + $rootScope.chatAliases.ChatFrame, [mcConst.containers.center]);
        }

        $rootScope.checkAbilityByRights();
    };

    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,
                    true, // enable Ranges

                    function (newInfo) {
                        if (newInfo.Ranges){
                            newInfo.Ranges = newInfo.Ranges.split(',');
                            newInfo.UINS = [];
                        } else {
                            newInfo.Ranges = [];
                            newInfo.UINS = newInfo.UINS.split(',');
                        }

                        newInfo.Ranges.forEach(function (_uins) {
                            if (_uins.indexOf("-") > 0){
                                var fromTo = _uins.split("-");

                                for (var i = fromTo[0]; i <= fromTo[1]; i++){
                                    newInfo.UINS.push(i);
                                }
                            } else {
                                newInfo.UINS.push(_uins);
                            }
                        });

                        newInfo.UINS = newInfo.UINS.join(',');

                        $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_newbies_to_txt_conf] = function sc_join_newbies_to_txt_conf(data){
        // UID UINS Moders
        var joinInfo = mcService.StringToObj(data);
        var uid      = joinInfo.UID;

        var usersList = joinInfo.UINS.split(",");
        var myUinIdx  = usersList.indexOf(mcConst.UserInfo.UIN.toString());

        if (myUinIdx !== -1){
            usersList.splice(myUinIdx, 1);
        }

        if (usersList.length){
            $rootScope.$broadcast(window._messages_.usersSmallInfo.getInfoByUserUINs, [
                usersList,

                function (updatedUsersList) {
                    updatedUsersList.forEach(function (user) {
                        if (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, usersList]);
                }
            ]);
        }
    };

    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(window._messages_.dialogsList.OpenPrivate, [info.ID]);
            break;

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

            case FORUM:
                $rootScope.SetForum();
                $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.Forum }]);
            break;

            case KANBAN:
                $rootScope.SetKanban();
                $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.Kanban }]);
            break;

            case BBS:
                $rootScope.SetBBS();
                $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.BBS }]);
            break;

            case BROADCAST:
                $rootScope.SetSendBroadcast();
                $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.SendBroadcast }]);
            break;

            case DEFAULT:
            break;

            case CREATE_NEW_TEXT_CONF:
                $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.CreateConf }]);
            break;

            case SELECT_CONF_TO_JOIN:
                $rootScope.$broadcast('show' + $rootScope.chatAliases.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);
                document.body.classList.toggle("dark", mcConst.ClientSettings.GeneralUITheme === "dark");
            } 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_user_list]     = function (data) {
    //     var info = mcService.StringToObj(data);
    //
    //     $rootScope.$broadcast('media_conf_user_list', [info]);
    // };

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

        $rootScope.$broadcast('media_conf_user_joined', [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_typing_notify_conf]            = 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, [$rootScope.chatAliases.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, [$rootScope.chatAliases.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' + $rootScope.chatAliases.Login, []);
        $rootScope.$broadcast(window._messages_.main.hideAllErrors);

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

        $rootScope.$broadcast('hide' + $rootScope.chatAliases.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' + $rootScope.chatAliases.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 (data) {
            destroySocketIo();

            customCallBack(_CMD_.ns_check_invite, data);
        });

        mcConnect.SetCallBackFunctionByCMD(_CMD_.Blocked, function (errCode) {
            // 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_.Login, function (info) {
            var data = info.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 (errCode) {
            // 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('/') > 0;

        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
                                ? mcConst._CMD_.loginStyle.AD
                                : (_PWD != undefined && _PWD != "" ? mcConst._CMD_.loginStyle.STANDARD : mcConst._CMD_.loginStyle.NO_PASS),
                // 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_.Register, function (info) {
            var data = info.split(mcConst.CR);

            mcConst.SessionID = data[0];

            tryRegister = true;

            //console.log('SessionID: ' + mcConst.SessionID);

            if ($rootScope.isWebClient){
                $rootScope.$broadcast('StartPingTimer', []);
            }
        });

        mcConnect.SetCallBackFunctionByCMD(_CMD_.Blocked, function (errCode) {
            // 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_.cs_login_by_token, function (info) {
            var data = info.split(mcConst.CR);

            mcConst.SessionID = data[0];

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

        mcConnect.SetCallBackFunctionByCMD(_CMD_.Blocked, function (errCode) {
            // var errCode = this;

            mcService.SaveSessionSettings("", "", "", false);

            $rootScope.$broadcast('enable' + $rootScope.chatAliases.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){
                // SEND GOT NOTIFY
                if (msg.Idx > confList.getLastMsgIdx(msg.UID) && msg.UIN != mcConst.UserInfo.UIN){
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_conf_got_message,
                        mcConst.SessionID,

                        msg.UID,
                        msg.Idx
                    ]);
                }

                // SEND READ NOTIFY
                read = (currentID == msg.UID && currentType === 'UID') && mcConst.isWindowFocused;
                if (msg.Idx > confList.getReadIdx(msg.UID) && read && msg.UIN != mcConst.UserInfo.UIN) {
                    $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)){

                // SEND GOT NOTIFY
                if (msg.Idx > privateList.getGotIdx(msg.UINFrom)) {
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_private_msg_got,
                        mcConst.SessionID,

                        msg.UINFrom,
                        msg.Idx
                    ]);
                }

                // SEND READ NOTIFY
                read = (currentID == msg.UINFrom && currentType === 'UIN') && mcConst.isWindowFocused;
                if (msg.Idx > privateList.getReadIdx(msg.UINFrom) && read){
                    $rootScope.SendCMDToServer([
                        mcConst._CMD_.cs_private_msg_read,
                        mcConst.SessionID,

                        msg.UINFrom,
                        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.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' + $rootScope.chatAliases.BBS);
        $rootScope.$broadcast('hide' + $rootScope.chatAliases.SendBroadcast);
        $rootScope.$broadcast('hide' + $rootScope.chatAliases.Forum);
        $rootScope.$broadcast('hide' + $rootScope.chatAliases.Admin);
        $rootScope.$broadcast('hide' + $rootScope.chatAliases.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' + $rootScope.chatAliases.BBS);
        $rootScope.$broadcast('hide' + $rootScope.chatAliases.SendBroadcast);
        $rootScope.$broadcast('hide' + $rootScope.chatAliases.Forum);
        $rootScope.$broadcast('hide' + $rootScope.chatAliases.Admin);
        $rootScope.$broadcast('hide' + $rootScope.chatAliases.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].MsgType === mcConst._CMD_.msgType.REACTION ||
                    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,
                            Reactions : source[i].Reactions,
                            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, more){
        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) {
                loadCount = mcConst.countHistoryMessagesLoad;
                range     = (range - mcConst.countHistoryMessagesLoad) + "-" + range;
            } else {
                loadCount = range - 1;
                range = "1-" + range;
            }

            $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.dtUTC   = msg.dt;
        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(window._messages_.chatWrapper.setChatViewAndTools, [{
                frame   : $rootScope.chatAliases.ChatFrame,
                // params  : 'dUIN-' + uin,
                params  : 'UIN-' + uin,
                tool    : $rootScope.chatAliases.PrivateInfo,
                fillArgs: [__getPrivateNick(uin), ""] //, (__getTopInListMsgIdx(uin) !== megaIdx) || __needHistory(uin)]
            }]);

            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;
                };
                userPrivate.getMaxNumb   = function () {
                    return Math.max(...userPrivate.messagesList);
                };
                userPrivate.getMinNumb   = function () {
                    return Math.min(...userPrivate.messagesList);
                };

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

                if (!userPrivate.hasOwnProperty("onlineState")) {
                    userPrivate.onlineState = 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.getMinNumb();//[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 {
                res = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [uin]);
                         
                if (!res) {
                    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 __findWhole(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(window._messages_.chatWrapper.setChatViewAndTools, [{
                frame   : $rootScope.chatAliases.ChatFrame,
                // params  : 'dUID-' + uid,
                params  : 'UID-' + uid,
                tool    : $rootScope.chatAliases.ConfUserList,
                fillArgs: [conf.ConfName, conf.ConfTopic, conf]
            }]);

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

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

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

                            delta = 50;
                        }

                        __getConfHistory(uid, delta);
                    }
                }
            }

            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    : 0,
                IDMsgGot    : newConf.IDMsgGot  ? newConf.IDMsgGot  : newConf.ConvID,
                IDMsgRead   : newConf.IDMsgRead ? newConf.IDMsgRead : newConf.ConvID,
                ConvID      : newConf.ConvID    ? newConf.ConvID    : 0,
                firstConvID : newConf.ConvID    ? newConf.ConvID    : 0, //megaIdx,
                hasMsgIdx   : function (idx) {
                    return this.messagesList.indexOf(idx) !== -1;
                }
            });

            __list[uid].getMaxNumb = function () {
                return Math.max(...__list[uid].messagesList);
            };
            __list[uid].getMinNumb = function () {
                return Math.min(...__list[uid].messagesList);
            };
        } 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;

            if (mcService.isString(_uins)){
                _uins = _uins.split(',');
            } else
            if (mcService.isArray(_uins)){
                
            } else {
                console.log("__addUsersToConf incorrect uins list:");
                console.log(_uins);
            }

            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.GetChatID() == -1) && $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      = null;

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

                if (uin && mcService.findItemInArrayOfObj(userList, uin, "UIN") !== -1){
                    // if (__statuses.getUserState(uin) >= mcConst.states.online ){
                    //     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]);
                    }
                }
            }

            uins.forEach(function (_uin) {
                userList.splice(mcService.findItemInArrayOfObj(userList, _uin, 'UIN'), 1);
            });
        }
    }

    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;

            var cnt    = count || mcConst.countHistoryMessagesLoad;
            var _from  = cnt;
            var _to    = 1;

            if (more){
                _from  = (__getFirstMsgIdx(uid) - mcConst.countHistoryMessagesLoad);
                _to    = (__getFirstMsgIdx(uid) - 1);

            } else {
                if (__list[uid].hasDirka) {
                    __list[uid].hasDirka = 0;
                }

                _from  = __getConf(uid, true).ConvID > cnt ? __getConf(uid, true).ConvID - cnt : 0;
                _to    = __getConf(uid, true).ConvID;
            }

            var range  = (_from <= 0 ? 1 : _from) + "-" + (_to < 0 ? count : _to || 1);
            var _count = _to - _from;

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

                uid,
                range,

                function (data) {
                    var msgCnt  = 0;
                    var deleted = 0;

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

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

                        if ((deleted >= _count/3) && msgCnt !== 0){
                            __getConfHistory(uid, _count < deleted ? 0 : _count - deleted, true);
                        }

                    } else {
                        msgCnt  = data.Data.length;
                        deleted = __showConfHistory(data, !!count);

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

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

            if (msg.MsgType === mcConst._CMD_.msgType.ADM_DELETE_MESSAGE ||
                msg.Mod     === mcConst._CMD_.msgMods.DELETED ||
                msg.MsgType === mcConst._CMD_.msgType.REACTION){

                needMore++;
            }

            if (msg.Mod === mcConst._CMD_.msgMods.DELETED || msg.MsgType === mcConst._CMD_.msgType.ADM_DELETE_MESSAGE || msg.MsgType === mcConst._CMD_.msgType.REACTION) {
                __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");
                var user         = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [msg.UIN]);

                msg.Idx          = msg.Idx || msg.MsgIdx;
                msg.UIN          = msg.UIN.toString();
                msg.Nick         = user && user.DisplayName ? user.DisplayName : "noName";
                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 __getConfMessagesCount(uid) {
        var conf = __getConf(uid, true);

        return conf && conf.messagesList ? conf.messagesList.length : 0;
    }

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

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

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

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

            // if (uid == 1003){
            //     console.log(idx);
            // }

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

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

                res = true;
            }

            if (conf.firstConvID >= idx || conf.firstConvID === 0){
                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 : 1;
    }

    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.getMinNumb();
            } else {
                delList           = conf.messagesList;
                conf.messagesList = [];
                conf.firstConvID  = __getLastMsgIdx(uid);// 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, _onResize) {
    "use strict";

    var _list = {};
    var self  = this;
    var chatTextOutput = null;
    var onWheel  = _onWheel  || null;
    var onResize = _onResize || 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();
            }

            if (onResize && typeof ResizeObserver !== 'undefined') {
                new ResizeObserver(function () {
                    onResize(element);
                }).observe(element);
            }

            _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) {
            if (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 loadImages   = new McLoadingImages($rootScope);
    var emojiList    = mcEmojiList.getPlainList();
    var parser       = new McMessageParser({
        emojiList      : emojiList,
        isWebClient    : $rootScope.isWebClient,
        thumbsSize     : $rootScope.thumbsSize,
        getDisplayName : function(uin){ return $rootScope.getDisplayName(uin); },
        getLocalHostPath: function(){ return mcService.getLocalHostPath($rootScope.isWebClient); },
        onLoadImage    : function(hash, idx){ loadImages.addImageToLoad(hash, idx); },
        onLoadImageNew : null,
        colorPalette   : $rootScope.colorPalette || [],
        getCurrentUIN  : function(){ return mcConst.UserInfo.UIN; },
        context        : 'chat'
    });
    var delMyMsgList = {};
    var loadAvatars  = [];
    var avatarTimer  = null;

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

    var RENDER_MSG = new function () {
        var bufferMsg       = [];
        var bufferHistory   = [];
        var newMsgTimer     = null;
        var historyMsgTimer = null;
        var firstDate       = {};
        var lastDate        = {};

        var constList = {
            normal: 1,
            custom: 2,
            kanban: 3,
            redirect: 4,
            files : 5,
            forum : 6,
            twitter: 7,
        };

        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;

                    // var firstMsg = msg.target.querySelector("._messagesFontSize:first-of-type");
                    // var firstMsg = msg.target.firstChild;
                    // var _date    = firstDate;//firstMsg ? firstMsg.getAttribute("postDate") : "";

                    if (firstDate[msg.frame] && msg.date !== firstDate[msg.frame] && (msg.target.getFirstMsg() && msg.target.getFirstMsg().className.indexOf("daySeparator") === -1)){
                        var daySeparator = document.createElement("span");
                        daySeparator.className = "byCenter brown daySeparator";
                        daySeparator.innerHTML = mcService.formatDate(firstDate[msg.frame], "— dd MM yyyy —");
                        // daySeparator.innerHTML     = "<span class='middleLine'>&nbsp;</span>" + mcService.formatDate(_date, "dd MM yyyy") + "<span class='middleLine'>&nbsp;</span>";

                        msg.target.addToHead(daySeparator);
                    }

                    switch (msg.__msgType) {
                        case constList.normal: case constList.twitter:
                            if (!msg.same) {
                                _br = document.createElement('span');
                                _br.className = "spanBr";

                                msg.date && _br.setAttribute("postDate", msg.date);
                                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.date && _br.setAttribute("postDate", msg.date);
                                msg.target.addToHead(_br);
                            }

                            msg.target.addToHead(msg.div);

                            if (msg.onAdd){
                                msg.onAdd();
                            }

                            if (msg.__msgType === constList.twitter){
                                setTimeout(function () {
                                    twttr.widgets.createTweet(
                                        msg.twId,
                                        document.getElementById("twttr-" + msg.msgIdx),
                                        {
                                            theme: "dark",
                                            width: 400
                                        }
                                    ).then(function () {
                                        Self.scrollDown && Self.scrollDown(frame, false, !msg.History, msg.History && msg.noScrollDown);
                                    }).catch(function (err) {
                                        console.warn(err);
                                    });
                                }, 50);
                            } else
                            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.date && _br.setAttribute("postDate", msg.date);
                            msg.target.addToHead(_br, true);
                            msg.target.addToHead(msg.div);

                            if (msg.onAdd){
                                msg.onAdd();
                            }
                        break;
                    }

                    _container = msg.target;

                    if (!firstDate[msg.frame] && !lastDate[msg.frame]) lastDate[msg.frame] = msg.date;

                    if (msg.date) firstDate[msg.frame] = msg.date;
                });

                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;

                    // var lastMsg = msg.target.querySelector("._messagesFontSize:last-of-type");
                    // var lastMsg = msg.target.lastChild;
                    // var _date    = lastDate; //lastMsg ? lastMsg.getAttribute("postDate") : "";

                    if (msg.date !== lastDate[msg.frame] && (msg.target.getFirstMsg() && msg.target.getFirstMsg().className.indexOf("daySeparator") === -1)){
                        var dd = msg.date || lastDate[msg.frame];

                        if (dd){
                            var daySeparator = document.createElement("span");
                            daySeparator.className = "byCenter brown daySeparator";
                            daySeparator.innerHTML = mcService.formatDate(dd, "— dd MM yyyy —");
                            // daySeparator.innerHTML     = "<span class='middleLine'>&nbsp;</span>" + mcService.formatDate(_date, "dd MM yyyy") + "<span class='middleLine'>&nbsp;</span>";

                            msg.target.addToFoot(daySeparator);
                        }
                    }

                    switch (msg.__msgType) {
                        case constList.normal: case constList.twitter:
                            if (!msg.same) {
                                _br = document.createElement('span');
                                _br.className = "spanBr";

                                msg.date && _br.setAttribute("postDate", msg.date);
                                msg.target.addToFoot(_br);
                            }

                            msg.target.addToFoot(msg.div);

                            if (msg.onAdd){
                                msg.onAdd();
                            }

                            if (msg.__msgType === constList.twitter){
                                setTimeout(function () {
                                    twttr.widgets.createTweet(
                                        msg.twId,
                                        document.getElementById("twttr-" + msg.msgIdx),
                                        {
                                            theme: "dark",
                                            width: 400
                                        }
                                    ).then(function () {
                                        Self.scrollDown && Self.scrollDown(frame, false, !msg.History, msg.History && msg.noScrollDown);
                                    }).catch(function (err) {
                                        console.warn(err);
                                    });
                                }, 50);
                            } else
                            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.date && _br.setAttribute("postDate", msg.date);
                            msg.target.addToFoot(_br);
                            msg.target.addToFoot(msg.div);

                            if (msg.onAdd){
                                msg.onAdd();
                            }
                        break;
                    }

                    if (msg.date) lastDate[msg.frame] = msg.date;
                });

                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(window._messages_.dialogsList.OpenConf, [id]);
                            } else {
                                $rootScope.$broadcast(window._messages_.dialogsList.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 __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     = 0;

        if (msg.hasOwnProperty("UINFrom")){
            uin = msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom;
        } else {
            uin = msg.UIN;
        }

        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 : uin // msg.UINFrom !== void(0) ? msg.UINFrom : msg.UIN,
            };

            var editParsed = parser.renderMessage({MsgType: mcConst._CMD_.msgType.TEXT, Msg: msgObj.MsgNew, Idx: idx, UINFrom: edit.uin}, {nick: "", uin: edit.uin});

            text.innerHTML   = editParsed ? editParsed.html : edit.text.join('<br/>');
            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 __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();
        var icon     = isWebSup ? mcService.getTwoLettersName(total.nick.replace("(websup)", "")) : mcService.getTwoLettersName(name);
        var 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> `
                : "";

        total.userName =
            `<div id="__${total.msgIdx}" class="grayBorderTop">`+
                `<div class="messageUserName lHeight20" uin="${total.uin}">`+
                    (noAvatar
                        ? ``
                        : `<span class="circleAvatarSmall shadow2px boxShadow finger smallChatAvatar${total.uin}" style="background-color: #${rColor}" onclick="window._userActions.openUserProfile(${total.uin})">${icon}</span>`) +
                    `<span class="_messageUserName finger" uin="${total.uin}" oncontextmenu="window._userActions.userMenu.apply(this)">${name}</span>`+
                    `<span class="messageTime messageTimeHistory">${badge}${total.date}</span>` +
                `</div>`+
            `</div>`;

        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 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 text          = total.text;
        var time          = total.time;
        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}'  style='word-break: break-word;' ` +
                `type='${total.msgType}' time='${total.dt}' ` +
                `onmouseover='window._userActions.hideStatus.apply(this)' ` +
                `onmouseout='window._userActions.showStatus.apply(this)' ` +
                // `oncontextmenu='window._messageActions.mainContext.apply(this)' ` +
                `>` +
                `<span id='${Idx}-st-${uin}' class='${state} statusItem ${uin != mcConst.UserInfo.UIN || total.isConf ? 'editRight' : ''}'>`+
                    (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} ${total.isTable ? "tableOverflow" : ""}'>${text}</span>`+

                parser.renderReactionBlock(total.reactions) +
            `</div>`;
    }

    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 __codeSnippet(msg, frame){
        var total = {
            frame  : frame,
            target : Self.framesList.get(frame, true),
            div    : document.createElement('div'),
            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";
        total.div.style.position  = "relative";

        total.nick = msg.Nick || msg.DisplayName || "cm_NoName";

        __userNameMessage(total);

        var parsed = parser.renderMessage(msg, { msgIdx: total.msgIdx });

        total.div.innerHTML =
            `${total.userName ? total.userName : ""}`+
            `<div id='hsMsg-${total.msgIdx}' class="relative _messagesFontSize showOnHover" type="${mcConst._CMD_.msgType.SOURCE_CODE}">`+
                `<span class="webix_icon fa-ellipsis-h msgMenu fAwesome needShow" Idx="${total.msgIdx}" type="${mcConst._CMD_.msgType.SOURCE_CODE}" uin="${total.uin}" time="${total.time}"></span>`+
                `<span class="messageTime" title="${total.date + ` ` + total.time}">${total.time}&nbsp;</span>`+
                `<pre  class="messageText messageTextP60" style="padding-top: 5pt; padding-bottom: 5pt; padding-left: 5pt">${parsed ? parsed.html : ""}</pre>`+
            `</div>`;

        total.div.setAttribute('uin', total.uin);

        var codeEl = total.div.querySelector(".messageText");
        codeEl.textContent = codeEl.textContent;
        hljs.highlightElement(codeEl);

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

        // if (msg.MsgType === mcConst._CMD_.msgType.FORWARD){
        //     msg = mcService
        // }

        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 || "userName",
            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,
            reactions  : msg.Reactions,
        };
        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;
        total.navIdx = total.isConf ? "conf-" + total.uid + "-" + total.msgIdx : "priv-" + (msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom) + "-" + total.msgIdx;

        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.navIdx);

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

        // --- Делегування парсингу контенту до McMessageParser ---
        var extraInfo = { msgIdx: total.msgIdx, nick: total.nick, uin: total.uin };

        // cText для IMAGE/IMAGE_NEW
        if (msg.MsgType === mcConst._CMD_.msgType.EXCEL_NEW || msg.MsgType === mcConst._CMD_.msgType.IMAGE_NEW) {
            var imageInfo = mcService.StringToObj(total.info[0]);
            extraInfo.cText = mcService.isString(imageInfo)
                ? (total.info.length > 5 ? mcService.copy(total.info, 5).join(mcConst.CRLF) : null)
                : imageInfo.Desc;
        } else if (msg.MsgType === mcConst._CMD_.msgType.IMAGE || msg.MsgType === mcConst._CMD_.msgType.CLIPBOARD_EXCEL) {
            extraInfo.cText = total.info.length > 5 ? mcService.copy(total.info, 5).join(mcConst.CRLF) : null;
        }

        var parsed = parser.renderMessage(msg, extraInfo);

        if (parsed) {
            total.text       = parsed.html;
            total.notifyText = parsed.notifyText;
            if (parsed.flags.twitter)      { total.twitter = true; total.twId = parsed.flags.twId; }
            if (parsed.flags.loadImageNew) { total.loadThmbNew = parsed.flags.loadImageNew; }
            else if (parsed.flags.loadImage) { total.loadThmb = true; }
            if (parsed.flags.isTable)      { total.isTable = true; }
            if (parsed.flags.hasAlign)     { total.hasAlign = true; }
            if (parsed.flags.onAdd)        { total.onAdd = parsed.flags.onAdd; }
            if (parsed.flags.hash)         { total.hash = parsed.flags.hash; }
        }

        __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, total.twitter ? RENDER_MSG.cost.twitter : 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] = parser.clearText(total.text[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 _messagesFontSize";
        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 =
            `<div class="relative">${total.userName ? total.userName : ``}`+
                (!noTimeStamp ? `<span class="messageTime" title="${total.date + ` ` + total.time}">${total.time}</span> ` : ``) +
                `<span class="messageText messageTextP60"><i class="fa fa-bullhorn" aria-hidden="true"></i> ${total.text}</span>`+
            `</div>`;

        total.div.setAttribute('uin', total.uin);
        total.div.setAttribute('postDate', total.date);

        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] = parser.clearText(total.text[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;" +
        }

        var parsed = parser.renderMessage(msg, { uin: total.uin });
        if (parsed) total.text = parsed.html;

        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) {
            var parsed = parser.renderMessage(msg, {});
            if (parsed) {
                total.text = parsed.html;
            }
            total.notifyText = "KANBAN: " + (total.kMSG.TaskName || total.kMSG.ProjectName || "");

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

        if (total.fMSG.Topic) {
            var parsed = parser.renderMessage(msg, {});
            if (parsed) {
                total.text = parsed.html;
            }

            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 __changeReaction(msg) {
        var msgObj  = mcService.StringToObj(msg.Msg);
        var isConf  = !!msg.UID;
        var idx     = msgObj.Idx;
        var uin     = 0;

        if (msg.hasOwnProperty("UINFrom")){
            uin = msg.UINFrom == mcConst.UserInfo.UIN ? msg.UINTo : msg.UINFrom;
        } else {
            uin = msg.UIN;
        }

        var message = document.getElementById(isConf ? `conf-${msg.UID}-${idx}` : `priv-${uin}-${idx}`);
        var el      = message ? message.getElementsByClassName("emjPlace")[0] : null;
        
        if (el) {
            el.outerHTML = parser.renderReactionBlock(msgObj.Reactions);
        }
    }

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

    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){
                    if (el.nextSibling && el.nextSibling.className === "spanBr"){
                        msgList.removeChild(el.nextSibling);
                    }

                    if (el.previousSibling && el.previousSibling.className.indexOf("daySeparator") > -1){
                        msgList.removeChild(el.previousSibling);
                    }

                    msgList.removeChild(el);
                }
            }
        }
    }

    // ======= DRAW MESSAGES =======================

    this.showNotify    = __showNotify;

    this.chatMessage   = function (msg) { // , currentFrame, frameNode
        var frame        = null;
        var _data        = null;
        var scrollHeight = 0;
        var __scrollDown = true;

        msg.Idx = msg.Idx || msg.MsgIdx;

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

        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:
            case mcConst._CMD_.msgType.FORMATTED_TEXT:
            case mcConst._CMD_.msgType.TABLE:
            case mcConst._CMD_.msgType.ACTION:
            case mcConst._CMD_.msgType.MULTILANG:
                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);
                    }
                }

                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.getDisplayName(msg.UINFrom)/*$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(window._messages_.dialogsList.OpenPrivate, [msg.UINFrom]);
                }
            break;

            // 38. MSG_TYPE_CONF_JUST_CREATED
                
            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.getDisplayName(msg.UIN); //$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(window._messages_.dialogsList.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;

            case mcConst._CMD_.msgType.CONF_JOIN:

            break;

            case mcConst._CMD_.msgType.CONF_LEAVE:
                
            break;

            case mcConst._CMD_.msgType.HISTORY_REMOVED: // історію конфи було повністю видалено
                msg.Text = mcLang({id: 383, from: "common"}); // "383":"Конференция была очищена",

                __addCustomMSG(msg, frame);
            break;

            case mcConst._CMD_.msgType.AUDIO:
                scrollHeight = __addMSG(msg, frame);
            break;

            case mcConst._CMD_.msgType.REACTION:
                if (!msg.History){
                    __changeReaction(msg, frame);
                }
            break;

            default:
                __addCustomMSG(msg, frame);
        }
    };

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

        switch (msg.MsgType) {
            case mcConst._CMD_.msgType.KANBAN_NOTIFY:
                $rootScope.$broadcast('updateCounter', [msg]);

                __addKanbanMSG(msg, frame);
            break;

            case mcConst._CMD_.msgType.SHOW_RECEIVED_FILES:
                __addReceivedFilesMSG(msg, frame);
            break;

            case mcConst._CMD_.msgType.SHOW_UPLOADED_FILES:
                __addReceivedFilesMSG(msg, frame);
            break;

            // case 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;
            //     }
            // break;

            default:
                if (msg.isTechMsg) {
                    if (msg.Place == 1) {
                        msg.UIN = msg.ID;

                        frame =  "UIN-" + msg.UIN;

                        $rootScope.$broadcast(window._messages_.dialogsList.OpenPrivate, [msg.UIN, function (){
                            __addTechMSG(msg, frame);
                        }]);
                    } else {
                        msg.UID = msg.ID;

                        frame =  "UID-" + msg.UID;

                        $rootScope.$broadcast(window._messages_.dialogsList.OpenConf, [msg.UID]);

                        __addTechMSG(msg, frame);
                    }

                    if (msg.StayOnTop){
                        $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                            mcConst._CMD_.ce_show_on_top
                        ]);
                    }
                } else {
                    __addCustomMSG(msg, frame);
                }
            break;
        }
    };

    this.getMyMessagesTemp = function () {
        return {
            UIN        : -1,
            IDMyMsgGot : 0,
            IDMyMsgRead: 0
        }
    };

    this.setMyMessages = function (data) {
        __myMessages = data;
    };

    this.removeMsgList = __removeMsgList;

    this.checkAnyMessage = function (frame) {
        var res = true;
        
        if (frame && frame.firstChild) {
            var msgList = frame.firstChild;

            if (msgList.getElementsByClassName("messageUser").length === 0){
                res = false;
            }
        }

        return res;
    };

    this.framesList     = null;
    this.loadAllImages  = loadImages.loadAllImages;
    this.scrollDown     = null;
}





function mcCallManager($scope, $rootScope, mcSound, __statuses) {
    "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 recordDuration = 3000;
    var recordFileName = "";
    var recordFileType = "";
    var pendingChunks  = 0;
    var finishCallback = 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);
        }

        if (questionBox) questionBox.close();

        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 && cPanel.cameraSwitch.disable();
            }

            if (!mcService.checkRights(mcConst._CMD_.RS.QPrivAllowMuteSoundDuringCalls)) {
                cPanel.microphoneSwitch && 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 (mcConst.ClientSettings.RecordMediaCalls == 1) {
                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) {
        pendingChunks++;
        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,
                    mcConst.ClientSettings.RecordMediaCallsDir
                ]);
            }

            pendingChunks--;
            if (pendingChunks === 0 && finishCallback) {
                finishCallback();
                finishCallback = null;
            }
        };

        reader.readAsArrayBuffer(blob)
    }

    function sendFinishMedia() {
        $scope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
            mcConst._CMD_.ce_finish_media
        ]);
    }

    function finishWhenReady() {
        if (pendingChunks === 0) {
            sendFinishMedia();
        } else {
            finishCallback = sendFinishMedia;
        }
    }

    function startRecord(streams){
        var fileNameMy = mediaEvents.isMyCall
            ? mcConst.UserInfo.UIN + "_" + getUIN() + "_" + mcService.formatDate(new Date(), "yyyy-mm-dd-hh-nn") + ".webm"
            : getUIN() + "_" + mcConst.UserInfo.UIN + "_" + mcService.formatDate(new Date(), "yyyy-mm-dd-hh-nn") + ".webm";
        var fileTypeMy = "video";

        recordFileName = fileNameMy;
        recordFileType = fileTypeMy;

        if (!streams.length) {
            console.error("startRecord: all streams is empty");
            return;
        }

        console.log("!Starting recording");

        // screen share: відео з потоку екрана + аудіо з усіх потоків (AudioContext мікс)
        if (mediaCall.receiveScreen) {
            var screenStream = mediaEvents.isMyCall
                ? streams[streams.length - 1]   // я ділюсь: мій екран — останній
                : streams[0];                   // я дивлюсь: екран співрозмовника — перший

            var audioCtx = new AudioContext();
            var audioDest = audioCtx.createMediaStreamDestination();
            streams.forEach(function(s) {
                if (s.getAudioTracks().length > 0) {
                    audioCtx.createMediaStreamSource(s).connect(audioDest);
                }
            });

            var combined = new MediaStream();
            screenStream.getVideoTracks().forEach(function(t) { combined.addTrack(t); });
            audioDest.stream.getAudioTracks().forEach(function(t) { combined.addTrack(t); });

            var mimeType = MediaRecorder.isTypeSupported('video/webm; codecs=vp9')
                ? 'video/webm; codecs=vp9'
                : 'video/webm';

            var nativeRecorder = new MediaRecorder(combined, { mimeType: mimeType });
            nativeRecorder.ondataavailable = function(e) {
                if (e.data && e.data.size > 0) {
                    saveBlob(e.data, fileNameMy, fileTypeMy);
                }
            };
            nativeRecorder.onstop = function() {
                audioCtx.close();
                finishWhenReady();
            };
            nativeRecorder.start(recordDuration);
            recorderRTC = nativeRecorder;
            return;
        }

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

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

        var rec = recorderRTC;
        var fname = recordFileName;
        var ftype = recordFileType;

        recorderRTC = null;
        recordFileName = "";
        recordFileType = "";

        if (rec.stopRecording) {
            // RecordRTC: чанки вже відправлені через ondataavailable, просто сигналізуємо завершення
            rec.stopRecording(function () {
                finishWhenReady();
                rec = null;
            });
        } else if (rec.stop) {
            // нативний MediaRecorder: ce_finish_media відправляється в onstop
            rec.stop();
            rec = null;
        }
    }

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

        recordFileName = fileNameMy;
        recordFileType = "audio";

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

        recorderRTC = new MediaRecorder(dest.stream);

        recorderRTC.ondataavailable = function (e) {
            if (e.data && e.data.size > 0) {
                saveBlob(e.data, fileNameMy, "audio");
            }
        };

        recorderRTC.onstop = function () {
            ctx.close();
            finishWhenReady();
        };

        recorderRTC.start(recordDuration);
    }

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

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

            questionBox.close();
        }

        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":"Вы отказались принять звонок",

            questionBox.close();
        }

        var btns = [];
        
        var cancelBTN = { view: "button", value: mcService.Lang(33), click: function () {
            reject(listCE.server_state_rejected);
        }};
        var audioBTN  = { view: "button", value: mcService.Lang(21), click: function (){
            answer(mcConst._CMD_.callType.VOICE);
            if (done) done();
        }};
        var videoBTN  = { view: "button", value: mcService.Lang(22), click: function () {
            answer(mcConst._CMD_.callType.VIDEO);
            if (done) done();
        }};
        var shareBTN  = { view: "button", value: mcService.Lang(659), click: function () {
            answer(mcConst._CMD_.callType.SHARING);
            if (done) done();
        }};

        switch (info.CallType){
            case mcConst._CMD_.callType.VIDEO:
                btns = [videoBTN, audioBTN, cancelBTN];
            break;

            case mcConst._CMD_.callType.VOICE:
                btns = [audioBTN,  cancelBTN];

            break;

            case mcConst._CMD_.callType.SHARING:
                btns = [shareBTN, cancelBTN];
            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.ui({
            view    : "window",
            css     : "mcWin callWin",
            position: "center",
            modal   : false,
            head    : mcService.Lang(39) + info.Nick, // "39" : "Входящий звонок",
            width   : 400,
            body    : {
                rows: [
                    { cols: btns }
                ]
            }
        });

        questionBox.show();
        
        $rootScope.$broadcast(window._messages_.chatFrame.focusToEnterChat);
    }

    // -- 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){
        if (recorderRTC) {
            stopRecord();
        }

        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();
            
            var _btn = function (btnView) {
                btnView.show();
                btnView.disable();
                setTimeout(function () {
                    if ($rootScope.GetChatType() === $rootScope.chatAliases.UIN &&
                        $rootScope.$broadcast(window._messages_.dialogsList.getUserState, [$rootScope.GetChatID()]) !== mcConst.states.offline)
                        btnView.enable();
                }, 2000);
            }

            _rights.QEnableVoiceCalls && !$rootScope.isWebClient && _btn($scope.startShareCall);

            _rights.QEnableVideoCalls && _btn($scope.startVideoCall);

            _rights.QEnableVoiceCalls && _btn($scope.startAudioCall)
        }
    }

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

        if (!$rootScope.isWebClient && skipCheck === "toolVideoCall"){
            $rootScope.$broadcast(window._messages_.clientData.sendCMDToElectron, [
                mcConst._CMD_.ce_grant_access_media,
                mcConst.mediaType.cam
            ]);
        } else
        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",
                            css     : "mcWin screenShareWin",
                            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)) {
                $scope.notInOfflineList = mcService.inArrayNoStrict(getUIN(), Statuses.Offline) === -1;
                $scope.inOnlineList     = mcService.inArrayNoStrict(getUIN(), Statuses.Online) >= 0;

                // юзер перелогинился и нам не пришел его статус офлайн, а сразу онлайн - отключаем звонок
                if (($scope.inOnlineList && nowCalling() && getUIN() == uin) ) {
                    _stopCall();
                }

                // две проверки при звонке
                if ($scope.inOnlineList || ($scope.notInOfflineList && status !== mcConst.states.offline && !isNaN(parseInt(status)))) {
                    !$rootScope.isWebClient && $scope.startShareCall.enable();
                    $scope.startVideoCall.enable();
                    $scope.startAudioCall.enable();
                } else
                if ((!$scope.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 && $scope.callBtsWrap.hide();
            $scope.stopCall && $scope.stopCall.show();
        } else {
            $scope.callBtsWrap && $scope.callBtsWrap.hide();
            $scope.stopCall && $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: 60, 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) { // access to microphone granted
            if (info.res){
                _startVoiceCall(true);
            } else {
                $rootScope.$broadcast('ErrorMsg', [-1, "<a href='darwin_open_access:mic'>Open microphone access</a>", "", function () {
                    
                }]);
            }
        } else
        if (info.type === mcConst.mediaType.cam) { // access to camera granted
            if (info.res){
                _startVideoCall(true);
            } else {
                $rootScope.$broadcast('ErrorMsg', [-1, "<a href='darwin_open_access:cam'>Open camera access</a>", "", function () {

                }]);
            }
        }
    });

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

    $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(window._messages_.dialogsList.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 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": mcConst._CMD_.ntfType.COMMON,
                "OnlyForVer": "",
                "id": 1
            },
            {
                "ForApp": "",
                "ForLicenseType": 0,
                "ForUsers": 0,
                "LockLang": "",
                "MsgBodyJSON": {},
                "MsgTitleJSON": {
                    "RU": "MyChat для Android",
                    "UA": "MyChat для Android",
                    "EN": "MyChat for Android"
                },
                "MsgType": mcConst._CMD_.ntfType.ANDROID_SETUP,
                "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": mcConst._CMD_.ntfType.COMMON,
                "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": mcConst._CMD_.ntfType.LINUXMACWEB,
                "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": mcConst._CMD_.ntfType.COMMON,
                "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": mcConst._CMD_.ntfType.UTP,
                "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": mcConst._CMD_.ntfType.COMMON,
                "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": mcConst._CMD_.ntfType.COMMON,
                "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": mcConst._CMD_.ntfType.UTP,
                "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": mcConst._CMD_.ntfType.GAME,
                "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": mcConst._CMD_.ntfType.COMMON,
                "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": mcConst._CMD_.ntfType.COMMON,
                "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": mcConst._CMD_.ntfType.COMMON,
                "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": mcConst._CMD_.ntfType.COMMON,
                "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": mcConst._CMD_.ntfType.COMMON,
                "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": mcConst._CMD_.ntfType.IOS_SETUP,
                "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": mcConst._CMD_.ntfType.FREE_VER_EXPIRED,
                "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("_getInfo: UIN-" + uin + ", " + (__users[uin] ? __users[uin] : "user not found or deleted"));
            }

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

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

    function _getUsersInfoByUins(uins, cb, invite) {
        if (mcService.isNumber(uins)){
            uins = uins.toString();
        } else
        if (mcService.isArray(uins)){
            uins = uins.join(',');
        }
        
        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 noBgInIcon",
                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} #{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",
        css   : "mcPopup loginInfoPopup",
        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",
        css   : "mcPopup loginInfoPopup",
        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
                }
            });

            var fixLineHeight = $$("loginLang").getPopup().getList();
            fixLineHeight.define("type", {height: 36});
            fixLineHeight.refresh();
        }

        login = webix.ui({
            id       : $scope.Name,
            view     : "window",
            css      : "mcWin loginWin",
            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);

    var fixLoginSelectServer = $scope.loginSelectServer;
    if (fixLoginSelectServer){
        fixLoginSelectServer.getPopup().getList().define("type", {height: 32});
        fixLoginSelectServer.refresh();
    }

    setTimeout(function () {
        $scope.loginField.focus();
    }, 10);

    login.show();

    return login;
}

﻿function loginController ($scope, $rootScope, $location){
    "use strict";

    $scope.Name = $rootScope.chatAliases.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.loginFormHidden = 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.loginFormHidden = 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');

        mcService.storage.put(mcConst.storageFields.lang, $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.loginFormHidden = 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' + $rootScope.chatAliases.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;

        mcService.storage.put(mcConst.storageFields.lang, $scope.lang);

        if (invite && $rootScope.isWebClient) {
            $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.loginFormHidden = false;
                    }
                }
            ]);
        } else

        if (!onlyChangeLang && $scope.remember) {
            $rootScope.loginFormHidden = !_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' + $rootScope.chatAliases.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.loginFormHidden = 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 = $rootScope.chatAliases.CommonContacts;

    var _view = null;
    var state = null;
    var userStatuses = null;

    $scope.filterField = null;
    $scope.data = [];

    function fillContacts(_data){
        var contacts = mcService.isArray(_data) ? _data : _data.Body;

        contacts.forEach(function(contact){
            if (contact.UIN !== -1) {
                contact.DisplayName = $rootScope.getDisplayName(contact.UIN); //$rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [contact.UIN]).DisplayName;
            }
        });

        _view.clearAll();
        _view.filter();
        _view.parse(mcService.convertDataToTree(contacts));

        $scope.data = contacts;

        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(window._messages_.dialogsList.OpenPrivate, [uin]);
        }
    };

    $scope.pinTool = function(){
        $rootScope.$broadcast($scope.pinBtn.getValue() ? window._messages_.chatWrapper.pinTool : window._messages_.chatWrapper.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 = $rootScope.chatAliases.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){
        var contacts = mcService.isArray(_data) ? _data : _data.Body;
        
        if (_view){
            contacts.forEach(function(user){
                if (user.UIN !== -1) {
                    user.DisplayName = $rootScope.getDisplayName(user.UIN); //$rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [contact.UIN]).DisplayName;
                }
            });
            
            _view.clearAll();
            _view.parse(mcService.convertDataToTree(contacts));

            $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(window._messages_.dialogsList.OpenPrivate, [uin]);
        }
    };

    $scope.pinTool = function(){
        $rootScope.$broadcast( $scope.pinBtn.getValue() ? window._messages_.chatWrapper.pinTool : window._messages_.chatWrapper.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 mcPopup userListMenu",
            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,
            css      : "mcPopup userListSubmenu",
            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 = $rootScope.chatAliases.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 " + mcService.getColorForStatus(mcConst.states.online) + "'></span>" +
                online + "/<span class='stateCircleInline " + 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(window._messages_.dialogsList.OpenPrivate, [$scope.contextUser]);
        }

        $scope.contextConfUser.hide();
    };

    $scope.getUserInfoFromContext = function (){
        if ($scope.contextUser) {
            if ($scope._rights.QAllowViewRemoteUserProfile){
                $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.UserProfile, params: $scope.contextUser }]);
            }
        }

        $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} #{statusColor}">&nbsp;</span> ' +
            '<span class="#{moder} #{userColor}">#{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: mcService.getColorForStatus(_status),
                name       : name,
                userColor  : mcService.getUserColorByStatus(_status)
            }
        )
    };

    $scope.openPrivate = function(id){
        var uin = $$(this).getItem(id).UIN;

        if (uin != mcConst.UserInfo.UIN){
            $rootScope.$broadcast(window._messages_.dialogsList.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.toLowerCase(), b.DisplayName.toLowerCase())
            : onlineA && onlineB
                ? mcService.sortAB(a.DisplayName.toLowerCase(), b.DisplayName.toLowerCase())
                : 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 initMediaConfUsers($scope){
    "use strict";

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

    if (!__view){
        var contacts = { id: $scope.Name, rows: [
            { height: 36, cols: [
                { id: "mediaConfLabelCount", template: mcLang(1701), css: 'byCenter noBG lHeight38', borderless: true} // "1701":"Учасники дзвінка",
            ]},

            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}
                ],
                on : {
                    onItemClick: $scope.openPrivate
                }
            }
        ]};

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

        __view = $$($scope.Name);

        mcService.disableContextMenu(__view);

        /*$scope.contextMediaConfUser = webix.ui({
            view   : "context",
            width  : 250,
            master : $scope.confUserList,
            id     : "contextMediaConfUser" + $scope.Name,
            css    : "noUpperCase mcPopup userListMenu",
            padding: 0,
            height : 60,
            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":"Открыть диалог",
            ]},
            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();
                    }
                }
            }
        });
*/
        $scope.confUserList        = $$("userList" + $scope.Name);
        $scope.mediaConfLabelCount = $$("mediaConfLabelCount");

    } else {
        __view.show();

        $scope.confUserList.clearAll();
        $scope.confUserList.parse($scope.data);
    }

    return __view;
}

function mediaConfUsersController($scope, $rootScope){
    "use strict";

    $scope.Name = $rootScope.chatAliases.MediaConfUsers;

    var _view       = null;
    var _currentUID = 0;

    $scope.data        = [];
    $scope.contextUser = null;

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

    function resolveDisplayName(user){
        var info = $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [user.UIN]);

        user.DisplayName = (info && info.DisplayName) ? info.DisplayName : ("UIN: " + user.UIN);

        return user;
    }

    function updateUserCount(){
        if ($scope.mediaConfLabelCount){
            $scope.mediaConfLabelCount.define("template",
                "<span>" + mcLang(1701) + " (" + $scope.data.length + ")</span>" // "1701":"Учасники дзвінка",
            );
            $scope.mediaConfLabelCount.refresh();
        }
    }

    function clearAll(){
        $scope.data = [];

        if ($scope.confUserList){
            $scope.confUserList.clearAll();

            updateUserCount();
        }
    }

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

    $scope.requestUsers = function(uid){
        $rootScope.$broadcast('SendCMDToServer', [
            mcConst._CMD_.cs_get_media_conf_user_list,
            mcConst.SessionID,
            uid,

            function(info){
                if (!info || !info.Users) return;

                $scope.data = info.Users.map(function(user){
                    user.id = user.UIN;
                    return resolveDisplayName(user);
                });

                $scope.container = $scope.container || mcConst.containers.right;

                if ($scope.confUserList){
                    $scope.confUserList.clearAll();
                    $scope.confUserList.parse($scope.data);
                }

                updateUserCount();
            }
        ]);
    };

    $scope.cnView = function(obj){
        var micIcon  = obj.Audio  ? "fa-microphone" : "fa-microphone-slash red";
        var camIcon  = obj.Video  ? "<i class='fa fa-video-camera' style='margin-left:3px'></i>" : "";
        var starIcon = obj.Creator ? "<i class='fa fa-star yellow' style='margin-left:3px'></i>" : "";

        return "<span class='fa " + micIcon + "' style='margin-right:3px'></span>" +
               "<span>" + (obj.DisplayName || "") + "</span>" +
               camIcon + starIcon;
    };

    $scope.sortList = function(a, b){
        return mcService.sortAB(
            (a.DisplayName || "").toLowerCase(),
            (b.DisplayName || "").toLowerCase()
        );
    };

    $scope.show = function(){
        $rootScope.setTool($scope.Name);

        _view = initMediaConfUsers($scope);

        updateUserCount();
    };

    $scope.openPrivate = function(id){
        var uin = $$(this).getItem(id).UIN;

        if (uin != mcConst.UserInfo.UIN){
            $rootScope.$broadcast(window._messages_.dialogsList.OpenPrivate, [uin]);
        }

        return false;
    };

    $scope.openDialogFromContext = function(){
        if ($scope.contextUser){
            $rootScope.$broadcast(window._messages_.dialogsList.OpenPrivate, [$scope.contextUser]);
        }

        // $scope.contextMediaConfUser.hide();
    };

    $scope.getUserInfoFromContext = function(){
        if ($scope.contextUser){
            $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.UserProfile, params: $scope.contextUser }]);
        }

        // $scope.contextMediaConfUser.hide();
    };

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

    var _msg = window._messages_.mediaConfUsers = {
        updateUserCount: 'mediaConfUsersUpdateCount'
    };

    $scope.$on('media_conf_user_list', function(e, args){
        var info = args[0];

        if (!info || !info.Users) return;

        $scope.data = info.Users.map(function(user){
            user.id = user.UIN;
            return resolveDisplayName(user);
        });

        $scope.container = $scope.container || mcConst.containers.right;

        $scope.show();

        if ($scope.confUserList){
            $scope.confUserList.clearAll();
            $scope.confUserList.parse($scope.data);
        }

        updateUserCount();
    });

    $scope.$on('media_conf_user_joined', function(e, args){
        var user = args[0];

        if (!user || !user.UIN) return;

        user.id = user.UIN;
        resolveDisplayName(user);

        $scope.data.push(user);

        if ($scope.confUserList){
            $scope.confUserList.parse([user]);
        }

        updateUserCount();
    });

    $scope.$on('media_conf_leave', function(e, args){
        var info = args[0];

        if (!info || !info.UIN) return;

        var idx = mcService.findItemInArrayOfObj($scope.data, info.UIN, "UIN");

        if (idx !== -1){
            mcService.delArrayItem($scope.data, idx);

            if ($scope.confUserList && $scope.confUserList.getItem(info.UIN)){
                $scope.confUserList.remove(info.UIN);
            }

            updateUserCount();
        }
    });

    $scope.$on('conf_status_change', function(e, args){
        var info = args[0];

        if (info && info.State === 0 && info.UID === _currentUID){
            clearAll();

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

    $scope.$on('hide' + $scope.Name, function(){
        if (_view){
            _view.hide();
        }
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        _currentUID = $rootScope.GetChatID();

        $scope.show();

        $scope.requestUsers(_currentUID);
    });

}

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 = $rootScope.chatAliases.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 class='piAppName' style='font-weight: bolder'>#{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(window._messages_.chatWrapper.setChatViewAndTools, [{
                frame : $rootScope.chatAliases.ChatFrame,
                // params: 'd' + $rootScope.GetChatTypeID(),
                params: $rootScope.GetChatTypeID(),
                tool  : $rootScope.chatAliases.PrivateInfo
            }]);

            $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 = $rootScope.chatAliases.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 ? window._messages_.chatWrapper.pinTool : window._messages_.chatWrapper.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 = $rootScope.chatAliases.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(window._messages_.dialogsList.OpenPrivate, [user.UIN]);
        }
    };

    $scope.pinTool = function(on){
        $rootScope.$broadcast( on ? window._messages_.chatWrapper.pinTool : window._messages_.chatWrapper.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 mcPopup dialogsMenu",
            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",
            css      : "mcPopup dialogsSubmenu",
            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 = $rootScope.chatAliases.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){
                    clearNotificationBlink();
                }
            } 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){
                clearNotificationBlink();
            }

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

    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' + $rootScope.chatAliases.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' + $rootScope.chatAliases.ChatWrapper);
                    } else {
                        $rootScope.SetPrivate(-1);
                        $scope.$broadcast(window._messages_.dialogsList.OpenPrivate, ['0']);
                    }
                }
            }

            $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(window._messages_.dialogsList.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(window._messages_.dialogsList.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.REACTION:
                msg = "<span>Reaction</span>" ;
            break;

            case mcConst._CMD_.msgType.AUDIO:
                msg = `<span>${mcLang(766)}</span>` ; // "766":"Аудиосообщение",
            break;

            case mcConst._CMD_.msgType.ACTION:
                msg = mcService.insertIco("fa-bolt") + `<span>Action</span>` ; //
            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} #{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' + $rootScope.chatAliases.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.setChatViewAndTools, [{ frame: $rootScope.chatAliases.CreateConf }]);
    };

    $scope.selectTools = function(id){
        $scope.dialogsList.unselectAll();
        $scope.channelsList.unselectAll();

        switch (id){
            case $rootScope.chatAliases.Admin + toolSuffix:
                $rootScope.SetAdmin();
            break;

            case $rootScope.chatAliases.Forum + toolSuffix:
                $rootScope.SetForum();
            break;

            case $rootScope.chatAliases.BBS + toolSuffix:
                $rootScope.SetBBS();
            break;

            case $rootScope.chatAliases.Kanban + toolSuffix:
                $rootScope.SetKanban();
                $rootScope.$broadcast(window._messages_.kanban.openKanbanUrl, [mcService.getLocalHostPath($rootScope.isWebClient) + "/" + mcConst.pathAliases.AliasKanban + "/"]);
            break;

            case $rootScope.chatAliases.SendBroadcast + toolSuffix:
                $rootScope.SetSendBroadcast();
            break;
        }

        tabSelect = null;

        $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: id.replace(toolSuffix, '') }]);
    };

    $scope.clickOnDialogItem = function (dialogType_ID) {
        var item   = $scope.dialogsList.getItem(dialogType_ID);

        if (item) {
            $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{
                frame: $rootScope.chatAliases.ChatFrame,
                params: dialogType_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(window._messages_.dialogsList.OpenPrivate, [item.UIN]);

            showOpenedPrivate();
        }
    };

    $scope.openChannelDialog = function(dUID){
        var item = $scope.channelsList.getItem(dUID);

        // var _badge = confList[id];

        if (item && item.Badge !== 0) {
            item.Badge = 0;

            Badges.update(dUID, item.Badge);
        }

        tabSelect = dUID;

        $scope.dialogsList.unselectAll();
        $scope.toolsList.unselectAll();

        $rootScope.$broadcast(window._messages_.dialogsList.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) || item.Speaker == mcConst.UserInfo.UIN) {
                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',
        clearNotificationBlink          : 'clearNotificationBlink',
        // updatePrivateCounter            : 'updatePrivateCounter'
    };

    $scope.$on(_msg.clearNotificationBlink, clearNotificationBlink);
    
    $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($rootScope.chatAliases.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: $rootScope.chatAliases.Admin  + '-tool'}] : []) //
           .concat($scope._rights.QKanbanAccess  ? [{ Name: mcService.insertIco("fa-tasks toolIcon")     + "Kanban",    Badge: 0, id: $rootScope.chatAliases.Kanban + '-tool'}] : []) //
           .concat($scope._rights.QForumAccess   ? [{ Name: mcService.insertIco("fa-bookmark toolIcon")  + mcLang(661), Badge: 0, id: $rootScope.chatAliases.Forum  + '-tool'}] : []) // "661":"Форум",
           .concat($scope._rights.QBBSView       ? [{ Name: mcService.insertIco("fa-calendar-o toolIcon")+ mcLang(556), Badge: 0, id: $rootScope.chatAliases.BBS    + '-tool'}] : []) // "556":"Доска объявлений",
           .concat($scope._rights.QBroadcasts    ? [{ Name: mcService.insertIco("fa-bullhorn toolIcon")  + mcLang(632), Badge: 0, id: $rootScope.chatAliases.SendBroadcast + '-tool'}] : []) // "632":"Оповещение",
           // .concat(!mcConst.UserInfo.hasInvite   ? [{ Name: mcService.insertIco("fa-star-o toolIcon")    + mcLang(719), Badge: 0, id: $rootScope.chatAliases.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 = $rootScope.chatAliases.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;

    $scope._rights = {
        QAllowOptionsChange: false,
        QAllowCloseProgram: false,
        QAllowAccountsManager: false,
        QWEBAllowViewSystemLog: false
    };

    function selectMenu(name){
        switch (name) {
            case "brcHistory":
                $scope.historyBroadcastBtn.callEvent("onItemClick");
            break;

            case $rootScope.chatAliases.Terminal:
                $scope.terminalBtn.callEvent("onItemClick");
            break;
        }
    }

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

    $scope.profile = function(){
        $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.UserProfile, params: mcConst.UserInfo.UIN }]);

        currentMenu = variants.PROFILE;
    };

    $scope.logs = function () {
        $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.ViewLogs }]);

        currentMenu = variants.LOGS;
    };

    $scope.settings = function () {
        $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.Settings }]);

        currentMenu = variants.SETTINGS;
    };

    $scope.terminal = function () {
        $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.Terminal }]);

        currentMenu = variants.TERMINAL;
    };

    $scope.brHistory = function(){
        $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.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()){
            view.hide();
        }

        $scope.removeHotKeys();
        $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 = $rootScope.chatAliases.Search;

    var view         = null;
    var currentRes   = 0;
    var currentID    = 0;
    var currentQuery = "";
    var SEARCH_USERS = "SEARCH_USERS";
    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){
        $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();

            $rootScope.$broadcast('show' + $rootScope.chatAliases.Dialogs, [mcConst.containers.left]);
        }

        currentQuery = "";
        currentRes   = [];
        currentID    = 0;
    }

    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(window._messages_.dialogsList.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 #{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(window._messages_.dialogsList.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",
            css     : "mcWin confManagerWin",
            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 = $rootScope.chatAliases.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.setChatViewAndTools, [{ frame: $rootScope.chatAliases.ChatFrame, params: UID }]);
        
        if (secure && !confList[UID]) {
            $rootScope.$broadcast("isConfOpen", [UID, function (here) {
                if (here){
                    $scope.hideWnd();

                    $rootScope.$broadcast(window._messages_.dialogsList.OpenConf, [uid]);
                } else {
                    $scope.passEnter();
                }
            }]);
        } else {
            $scope.hideWnd();

            $rootScope.$broadcast(window._messages_.dialogsList.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 = $rootScope.chatAliases.HistoryBroadcast;

    var view          = null;
    var selectedUsers = null;
    var duManager     = null;
    var selectTimer   = 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($rootScope.chatAliases.HistoryBroadcast, [{
            key   : mcConst.keyCodes.esc,
            lockPrev: true,
            func  : function () {
                $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{}]);
            }
        }], document, true);
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset($rootScope.chatAliases.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];

        $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",
            css     : "mcWin broadcastWin",
            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 = $rootScope.chatAliases.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",
            hidden  : true,
            rows    : [
                { height: 37, cols: [
                    { id: "sbrTitle",  template: mcService.insertIco("fa-bullhorn toolIcon") + mcLang(623), css: "byCenter lHeight36 toUpper whiteBG", 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:[
                            { 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":"Файлы",
                            ], on: {
                                onViewResize: $scope.onInputResize
                            }},

                            { 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)
                                        }
                                    );
                                }
                            }
                        ]}
                    ], on: {
                        onViewResize: $scope.onInputResize
                    }},

                    { 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) {
                                        name = (obj.$level > 1 ? mcService.insertIco("fa-users", "smaller") : "") + "<span class='treeBranchContacts webix_tree_item green'>" + 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 class='gray' style='" + (mcService.isFuckingFirefox() ? "" : "float:right; ") + "margin-right: 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 = $rootScope.chatAliases.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($rootScope.chatAliases.SendBroadcast, [{
            key   : mcConst.keyCodes.enter,
            ctrlKey: true,
            func  : $scope.sendBroadcast
        }], $scope.sbrText.getInputNode());
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset($rootScope.chatAliases.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) {
                        var contacts = mcService.isArray(_data) ? _data : _data.Body;

                        contacts.forEach(function(contact){
                            if (contact.UIN !== -1) {
                                contact.DisplayName = $rootScope.getDisplayName(contact.UIN); //$rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [contact.UIN]).DisplayName;
                            }
                        });

                        contacts = convertToTree(contacts, userTreeID);

                        defaultGroupList[2].data = contacts[0];
                        userTreeID               = contacts[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) {
                        var contacts = mcService.isArray(_data) ? _data : _data.Body;

                        contacts.forEach(function(user){
                            if (user.UIN !== -1) {
                                user.DisplayName = $rootScope.getDisplayName(user.UIN); //$rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [contact.UIN]).DisplayName;
                            }
                        });
                        
                        contacts = convertToTree(contacts, userTreeID);

                        defaultGroupList[3].data = contacts[0];
                        userTreeID               = contacts[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.onInputResize = function () {
        // downloadUploadManager && $scope.sbrText && downloadUploadManager.onPasteItemFromClipboard($scope.sbrText.getInputNode());
    };

    $scope.show = function(broadcasts){
        if (!view){
            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();

                            // setTimeout(function () {
                            //     downloadUploadManager.onPasteItemFromClipboard($scope.sbrText.getInputNode());
                            // }, 100);
                        }
                    }
                }
            });

            $scope.uploadFile = downloadUploadManager.uploadFile;
            $scope.initDU     = downloadUploadManager.initViews;
            $scope.dwlMngr    = downloadUploadManager.getViews(mcLang(33)); // "33" :"Отмена",

            view = initSendBroadcastView($scope, broadcasts);

            downloadUploadManager.setPastePreviewWindow(function imagePastePreview(image, cb) {
                webix.confirm({
                    width : 350,
                    height: 350,
                    position: "center",
                    type  : "confirm-warning", // "641":"Добавить файл",
                    text  : `${mcLang(641)}<br><br><div style='height: 235px; vertical-align: center'><img src='${window.URL.createObjectURL(image)}' style='max-height: 230px; max-width: 330px'></div>`,
                    ok    : mcService.Lang(13), // "13" :"Ок",
                    cancel: mcService.Lang(33), // "33": "Отмена",
                    callback: function(yes){
                        yes && cb();

                        // focusToEnterChat();
                    }
                });
            });
            downloadUploadManager.setPastePreviewWindowFile(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(641) + "<br><br>" + list, // "641":"Добавить файл",
                    ok    : mcService.Lang(13), // "13" :"Ок",
                    cancel: mcService.Lang(33), // "33": "Отмена",

                    callback: function(yes){
                        yes && cb();

                        // focusToEnterChat();
                    }
                });
            });

            downloadUploadManager.onPasteItemFromClipboard($scope.sbrText.$view);

            $scope.initDU();
        }

        view.show();

        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 bgMiniUserPhoto"},
                    
                    { 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,
                    // onContext: {},
                    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
                    // on: {
                    //     onBeforeContextMenu: function (id, e) {
                    //         console.log(e.target);
                    //         e.preventDefault();
                    //     }
                    // }
                },

                { height: 20, cols: [
                    { id: 'framePrivateTypingTextNotify', template: ' ', borderless: true, css: "noBGColor font90 paddingLeft7 lHeight20", gravity: 1},
                    { gravity: 0},
                    { 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, on: {
                onAfterRender : function(){
                    $scope.inputResizer._viewobj.addEventListener('mousedown', function () {
                        var stick = $scope.inputResizer._resizeStick;
                        if (stick && stick._viewobj) {
                            // _viewobj !== null означає orphaned stick (mouseup був пропущений)
                            webix.eventRemove(stick._moveev);
                            webix.eventRemove(stick._upev);
                        }
                        $scope.inputResizer._resizeStick = null;
                    }, true);
                }
            }},

            // -- 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 bgMCtextarea", 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 mcPopup chatAttachMenu",
        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: "codeBtn" + $scope.Name, view: "button", type: "icon", icon: "code", css: "icoButton", height: 27,
                label: "Code", click: $scope.insertCodeToChat },  // 

            // { 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 mcPopup chatEmojiMenu",
        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 mcPopup chatTopMenu",
        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 mcPopup chatUserMenu",
        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",
        css      : "mcPopup chatSubmenu",
        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",
        css      : "mcPopup chatSubmenu",
        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.codeBtn           = $$("codeBtn"        + $scope.Name);

    // $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 = $rootScope.chatAliases.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 typingUIN         = 0;
    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 canScrollDown     = true;
    var autoDownHeight    = 150;

    var old_shit_type = {
        smile: 1
    };

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

    var framesList            = new McFramesList(_onWheel, _onResize);
    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._messageActions.mainContext = function () {
        var el = this;

        $scope.inChatClick.msgMenu({ target: el }, true);
    };

    window._userActions.userMenu      = function (){
        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 _setTxtToEnterChat(txt) {
        if (!txt){
            $scope.enterChatText.setValue('');
            $scope.enterChatText.$setValue('');
        } else {
            $scope.enterChatText.setValue(txt);
        }
    }

    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){
                    var _msg = mcService.StringToObj(replMsg.Msg);
                    
                    replMsg = {
                        "UIN"    : parseInt(replMsg.UINFrom),
                        "Msg"    : _msg.Reply.Msg,
                        "Idx"    : parseInt(replMsg.Idx),
                        "MsgType": mcConst._CMD_.msgType.TEXT,
                        "dtUTC"  : replMsg.dtUTC
                    }
                } else {
                    replMsg = {
                        "UIN"    : parseInt(replMsg.UIN),
                        "Msg"    : replMsg.Msg,
                        "Idx"    : parseInt(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){
                var orig = $rootScope.$broadcast(window._messages_.dialogsList.getConfHistoryMsg, [UID, replyToMsgIdx]);

                msg = JSON.stringify({
                    "Original": {
                        "UIN"    : parseInt(orig.UIN),
                        "Msg"    : orig.Msg,
                        "Idx"    : parseInt(orig.Idx),
                        "MsgType": parseInt(orig.MsgType),
                        "dtUTC"  : orig.dtUTC
                    },
                    "Reply"   : text ? {
                        "Msg"    : mcService.StringToObj(text),
                        "MsgType": parseInt(msgType)
                    } : {
                        "Msg"    : msg,
                        "MsgType": msgType === void(0) ? mcConst._CMD_.msgType.TEXT : parseInt(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 && (canScrollDown || (history && scrollHeight))){
            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);

                _setTxtToEnterChat("");
            });

            titleValue    = [topBtnTitle, topTopic];
            prevFrame     = frameID;
            currentFrame  = framesList.get(frameID);
            canScrollDown = true;
            
            $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();
                    }

                    $scope.displayName.define('tooltip', "");
                    // $$("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();

                    $scope.displayName.define('tooltip', $scope.chatHint());
                break;

                default:
                    $$($scope.container).getNode().className = "webix_view noMargin webix_layout_line";
                    $scope.displayName.define('tooltip', "");
            }

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

            pressNotifyCount = 0;
            stopDrawTypingNotify();

            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 = $$("enterChatText" + $scope.Name);
                
                $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(){
                _setTxtToEnterChat('');

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

            mainView.getTopParentView().adjust();
        } 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]);

            _setTxtToEnterChat(txt);

            focusToEnterChat(txt.length);
        } else {
            _setTxtToEnterChat("");

            $rootScope.Storage.msgTemplate.load(
                mcConst.ServerInfo.ID,
                mcConst.UserInfo.UIN,
                id,

                function(tempMsg) {
                    saveMsgTempList[id] = tempMsg || "";

                    if (tempMsg){
                        var txt = _get(tempMsg);

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

        var distanceToBottom = chatView.getScrollH() - chatView.scrollTop - chatView.clientHeight;

        if (distanceToBottom <= autoDownHeight){
            canScrollDown = true;
        } else {
            canScrollDown = false;
        }

        if (chatView.snapToBottomTimer) {
            clearTimeout(chatView.snapToBottomTimer);
            chatView.snapToBottomTimer = null;
        }

        var scrollingDown = chatView.scrollTop >= (chatView.prevScrollTop || 0);
        chatView.prevScrollTop = chatView.scrollTop;

        if (distanceToBottom > 0 && distanceToBottom <= autoDownHeight && scrollingDown) {
            chatView.snapToBottomTimer = setTimeout(function () {
                chatView.snapToBottomTimer = null;

                if (chatView.getScrollH() - chatView.scrollTop - chatView.clientHeight <= autoDownHeight) {
                    scrollDown(chatView.chatType + "-" + chatView.chatID, false, true);
                }
            }, 1000);
        }

        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 _onResize(chatView) {
        if (canScrollDown && frameID === chatView.chatType + "-" + chatView.chatID) {
            scrollDown(frameID, true);
        }
    }

    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 {
                    _setTxtToEnterChat(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 {
                    _setTxtToEnterChat(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 && _setTxtToEnterChat(text);

        $rootScope.hotKeyDispatcher.addPreset(EDITPRESET, [{
            key   : mcConst.keyCodes.esc,
            lockPrev: true,
            func  : function () {
                $scope.closeEditMsg();
            }
        }], document);

        focusToEnterChat();
    }

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

    function stopDrawTypingNotify(){
        clearTimeout(timerTypingNotify);

        timerTypingNotify = null;
        typingUIN = 0;

        if (mainView && mainView.className && mainView.className.indexOf('noBG') !== -1) {
            mainView.className = mainView.className.replace(' noBG', "");
        }

        $scope.PrivateTypingTextNotify.define('template', ' ');
        $scope.PrivateTypingTextNotify.refresh();
    }

    function startDrawTypingNotify(info){
        if (
            (mainView && mainView.isVisible()) &&
            (
                (info.UIN == $rootScope.GetChatID() && $rootScope.GetChatType() === "UIN") ||
                (info.UID == $rootScope.GetChatID() && $rootScope.GetChatType() === "UID")
            )
        ){
            // var notifyView = mcService.getFrame('framePrivateTypingNotify');
            //
            // if (notifyView && notifyView.className.indexOf('noBG') === -1) {
            //     notifyView.className += ' noBG';
            // }

            stopDrawTypingNotify();

            var userName = $rootScope.getDisplayName(info.UIN); //$rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [info.UIN]).DisplayName || "";

            typingUIN = info.UIN;

            $scope.PrivateTypingTextNotify.define('template', mcService.insertIco("fa-pencil") + " " + userName + " " + mcService.Lang(49) + "&nbsp;&nbsp;"); // "49" : "пишет...",
            $scope.PrivateTypingTextNotify.refresh();
        }

        timerTypingNotify = setTimeout(function(){
            stopDrawTypingNotify();
        }, 4000);
    }

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

    $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");
                    $rootScope.$broadcast(window._messages_.chatWrapper.selectTool,
                        [$rootScope.chatAliases.MediaConfUsers]);
                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');
            $rootScope.$broadcast('hide' + $rootScope.chatAliases.MediaConfUsers);
        });
    };

    $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.chatHint = function (obj) {
        var res = null;

        switch ($rootScope.GetChatType()){
            case $rootScope.chatAliases.UID:
                res = `UID: ${$rootScope.GetChatID()}`
            break;

            case $rootScope.chatAliases.UIN:
                res = `UIN: ${$rootScope.GetChatID()}`
            break;
        }

        return res;
    };
    
    $scope.scrollDown = function () {
        canScrollDown = true;
        
        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(window._messages_.dialogsList.OpenPrivate, [contextUser]);

            $scope.userContextMenu.hide();
        }
    };

    $scope.getUserInfoFromContext = function (){
        if (contextUser) {
            if ($scope._rights.QAllowViewRemoteUserProfile){
                $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{ frame: $rootScope.chatAliases.UserProfile, params: contextUser }]);
            }
        }

        $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, isMsg) {
            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(',');

            if (!isNaN(msgType)){
                var replyHint = mcLang(725); // "725":"Ответить";
                var delHint   = mcLang(519); // "519":"Удалить";
                var editHint  = mcLang(761); // "761":"Редактировать сообщение",
                var copyHint  = mcLang(765); // "765":"Скопировать текст",
                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();
                }

                var _showReply = msgType !== mcConst._CMD_.msgType.SOURCE_CODE;
                var _showCopy  = $scope._rights.QSecurityAllowChatTextSelect && (msgType !== mcConst._CMD_.msgType.IMAGE_NEW || msgType !== mcConst._CMD_.msgType.FILE_NEW);
                var _showDel   = (isConf && canDelConfMsg)  || (!isConf && canDelPrivMsg);
                var _showEdit  = ((isConf && canEditConfMsg) || (!isConf && canEditPrivMsg)) && msgType === mcConst._CMD_.msgType.TEXT && uin == mcConst.UserInfo.UIN && delta <= mcConst.UserInfo.MaxSelfMsgEditTime;
                
                wndMsgTools = webix.ui({
                    view  : "popup",
                    css   : "mcPopup msgToolsMenu",
                    width : 225,
                    height: 17 * (_showReply + _showEdit + _showDel + _showCopy + 1),
                    // paddingX: 7,
                    body  : {
                        // css: "byCenterTools",
                        padding: 0,
                        borderless: true,
                        onClick: $scope.msgToolsAction,
                        template: `
                        ${_showReply ? `<div Idx='${Idx}' class='finger replyMessage'><span class='webix_icon fa-reply fAwesome'    title='${replyHint}'></span>${replyHint}</div>` : ``}
                        ${_showCopy  ? `<div Idx='${Idx}' class='finger copyMessage'><span class='webix_icon fa-files-o fAwesome'   title='${copyHint}'></span>${copyHint}</div>` : ``}
                        ${_showDel   ? `<div Idx='${Idx}' class='finger deleteMessage'><span class='webix_icon fa-trash-o fAwesome' title='${delHint}'></span>${delHint}</div>` : ``}
                        ${_showEdit  ? `<div Idx='${Idx}' class='finger editMessage'><span class='webix_icon fa-pencil  fAwesome'   title='${editHint}'></span>${editHint}</div>` : ``}
                        
                    `
                    },
                    on: { onClose: function () {
                            wndMsgTools = null;
                        }}
                });

                if (isMsg){
                    wndMsgTools.show({x: $rootScope.getMouseX(), y: $rootScope.getMouseY() + 30});
                } else {
                    wndMsgTools.show(menu, {pos: "bottom", x: -10, y: 6});
                }
            }
        },

        "emjClick"           : function (ev) {
            var emj  = ev.target;
            var What = emj.getAttribute("code");
            var Place= $rootScope.GetChatType() === "UIN" ? 1 : 2;
            var ID   = $rootScope.GetChatID();
            var Idx  = emj.parentElement.parentElement.parentElement.getAttribute("idx");

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

                Place, // 1 - приват, 2 - конференция
                ID,    // UIN либо UID
                Idx,   // индекс сообщения, к которому добавляется реакция
                What   // unicode-emoji symbol
            ]);
        },

        "_openAction"        : function (ev) {
            var action   = parseInt(ev.target.getAttribute("action"));

            switch (action){
                case 19:
                    $rootScope.$broadcast(window._messages_.chatWrapper.toggleTool, [$rootScope.chatAliases.PersonalContacts]);
                break;
            }
        }
    };

    $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();
            var msgType   = parseInt(msgDIV.getAttribute("type"));

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

            if (msgType === mcConst._CMD_.msgType.SOURCE_CODE){
                msgDIV.innerHTML = "Source Code";
            }

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

        "copyMessage"        : function (ev) {
            var msgDIV  = document.getElementById(`hsMsg-${ev.target.getAttribute('Idx')}`).cloneNode(true);
            var childList = msgDIV.querySelectorAll('.messageText');
            var text    = "";

            if (wndMsgTools) wndMsgTools.hide();
            
            childList.forEach(function (item){
                if (item.className.indexOf("messageText") >= 0){
                    item.childNodes.forEach(function (elm) {
                        switch (elm.nodeName){
                            case "#text":
                                text += elm.data;
                            break;

                            case "SPAN":
                                text += elm.innerText;
                            break;

                            case "PRE":
                                text += elm.innerText;
                            break;

                            case "A":
                                text += elm.innerText;
                            break;

                            case "BR":
                                text += "\n";
                            break;
                        }
                    });
                }
            });

            mcService.copyTextToClipboard(text);
            focusToEnterChat();
        }
    };

    $scope.sendSmile = {
        "finger": function(ev, id){
            // var smile = "L$" + mcConst.terminator2 + ")" + $rootScope.smileSetNumb + mcConst.terminator3 + ev.target.title + mcConst.terminator2;

            // _sendText(null, null, smile, mcConst._CMD_.msgType.OLD_SHIT, old_shit_type.smile);

            // this.getTopParentView().hide();

            var emoji = ev.target.getAttribute("emoji");
            
            cursorPosition = mcService.insertAtCursor($scope.enterChatText.getInputNode(), emoji);

            focusToEnterChat();

            return false; // blocks the default click behavior
        }
    };
    
    $scope.popupInsertPhrase = function(){

    };

    $scope.sendText = _sendText;

    $scope.popupInsertFile = function(){
        this.getTopParentView().hide();

        $scope.uploadFile(mcConst._CMD_.msgType.FILE_NEW, false, false);
    };

    $scope.insertCodeToChat = function () {
        var idTxt = "code" + mcService.RandomHash(7);

        var wnd = webix.ui({
            view    : "window",
            position: "center",
            css     : "mcWin",
            modal   : true,
            width   : $rootScope.wndSize.width * 0.75,
            height  : $rootScope.wndSize.height * 0.75,
            head    : { cols:[
                { width: 42 },
                { view : "label",  label:'<div class="byCenter toUpper">Code snippet</div>'}, // 
                { view : "button", type: 'icon', width: 42, icon: 'times', css: "button_transparent", click: function () {
                    wnd.hide();
                    wnd.destructor();
                } }
            ]},
            body    : { rows: [
                { view: "textarea", id: idTxt, css: "borderTop" },
                { cols: [
                    {},
                    { view: "button", value: mcLang(19), click: function(){ //"19" :"Отправить",
                        var resText = mcService.trim(codeText.getValue());

                        if (resText){
                            var id       = $rootScope.GetChatID();
                            var chatType = $rootScope.GetChatType();

                            // resText = resText.replace(/\n/g, mcConst.CRLF);
                            resText = JSON.stringify({
                                Msg : resText,
                                // HTML: hljs.highlight(resText, {language: "java"}).value,
                                Lang: "JavaScript"
                            });

                            switch (chatType){
                                case 'UIN': sendPrivate(id, resText, mcConst._CMD_.msgType.SOURCE_CODE); break;
                                case 'UID': sendConf   (id, resText, mcConst._CMD_.msgType.SOURCE_CODE); break;
                            }
                        }

                        wnd.hide();
                        wnd.destructor();
                    }}
                ]},
                { height: 6}
            ]}
        });

        this.getTopParentView().hide();

        var codeText = $$(idTxt);

        wnd.show();

        codeText.getInputNode().addEventListener("paste", function (e) {
            var raw = e.clipboardData.items[0];
            var data;

            if (raw && raw.kind === "string"){
                raw.getAsString(function (s) {
                    data = s;

                    if (data){
                        var min = 999999;

                        data = data.replace(/\r\n/gi, "\n");
                        data = data.split('\n');
                        
                        data.forEach(function (line, i){
                            if (mcService.trim(line) !== ""){
                                var len = line.split(/[^\s]+/)[0].length;

                                if (len < min) {
                                    min = len;
                                }
                            } else {
                                data[i] = "";
                            }
                        });

                        data.forEach(function (line, i) {
                            if (line.length > min){
                                data[i] = data[i].replace(new RegExp("^\\s{" + min + "}"), "");
                            }
                        });

                        codeText.setValue(data.join('\n'));
                    }
                });
            }
        });

        codeText.focus();
    };

    $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) {
                var available = $scope.inputWrapper.getParentView().$height;
                var maxAllowed = available - 107; // 100 мін висота чату + 7 resizer
                var clamped = Math.max(mcConst.minEnterTextHeight, Math.min(height, maxAllowed));
                $scope.inputWrapper.define('height', clamped);
                $scope.inputWrapper.resize();
                $scope.inputWrapper.resizeChildren();
            }
        });
    };

    $scope.closeReplyMsg = function (){
        if (replyToMsgIdx) {
            replyToMsgIdx = 0;

            $scope.replyBlockWrapper.hide();
            $rootScope.hotKeyDispatcher.removePreset(REPLAYPRESET);

            focusToEnterChat();
        }
    };

    $scope.closeEditMsg = function (){
        if (editMsgIdx) {
            editMsgIdx = 0;

            $scope.editMessageWrapper.hide();
            $rootScope.hotKeyDispatcher.removePreset(EDITPRESET);
            _setTxtToEnterChat("");

            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($rootScope.chatAliases.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){
                    _setTxtToEnterChat(selectedText.prefix + transformRusText(selectedText.infix) + selectedText.postfix);

                    mcService.setSelectionText(node, selectedText.selectionStart, selectedText.selectionEnd);
                } else {
                    _setTxtToEnterChat(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($rootScope.chatAliases.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, [$rootScope.chatAliases.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, [$rootScope.chatAliases.Terminal]);
            }
        }], document);

        $rootScope.hotKeyDispatcher.addPreset($rootScope.chatAliases.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($rootScope.chatAliases.ChatFrame);
        $rootScope.hotKeyDispatcher.removePreset($rootScope.chatAliases.ChatFrame + "global");
        $rootScope.hotKeyDispatcher.removePreset($rootScope.chatAliases.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(window._messages_.chatWrapper.toggleTool, []);
            // break;
            //
            // case $rootScope.chatAliases.UID:
            //     $rootScope.$broadcast(window._messages_.chatWrapper.toggleTool, [$rootScope.chatAliases.ConfUserList]);
            // 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() === 'UID') {
            pressNotifyCount ++;

            if (pressNotifyCount === 10){
                pressNotifyCount = 0;

                $rootScope.$broadcast('SendCMDToServer', [
                    $rootScope.GetChatType() === 'UID' ? mcConst._CMD_.cs_typing_notify_conf : 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); // todo: замінити на колбек який буде вертати вью

            $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.chatTextOutputW.getNode().oncontextmenu = function (e) {
                var _selection = window.getSelection();

                if (_selection && _selection.type === "range"){
                    //var text = _selection.getRangeAt(1).toString();
                }

                // e.preventDefault();
            }

            $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) {
                _setTxtToEnterChat(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 == typingUIN || msg.UIN == typingUIN)  {
                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();

            // if (!messageRenderer.checkAnyMessagePriv(priv)) {
            // }
        }

        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){
        // "UIN" : 101, // идентификатор пользователя, который пишет
        // "UID" : 2, // идентификатор конференции, в которую отправляется уведомление "печатаю"
        // "What" : 0, // необязательный параметр. Что именно делает юзер, 1 - просто набирает, 45 - отправка файла, 44 - отправка изображения
        // // (msg_type_...)
        // "AppType" : 0 // тип клиента
        // CLIENT_TYPE_MYCHAT_UNKNOWN    = -1;
        // CLIENT_TYPE_MYCHAT_WIN32      = 0;
        // CLIENT_TYPE_MYCHAT_WEB        = 1;
        // CLIENT_TYPE_MYCHAT_MACOS      = 2;
        // CLIENT_TYPE_MYCHAT_LINUX      = 3;
        // CLIENT_TYPE_MYCHAT_ANDROID    = 4;
        // CLIENT_TYPE_MYCHAT_IOS        = 5;
        // CLIENT_TYPE_MYCHAT_WIN32NEXT  = 6;
        // CLIENT_TYPE_MYCHAT_SERVER_API = 7; // сообщение отправлено с сервера, через API или из скрипта
        var info = args[0];

        startDrawTypingNotify(info);
    });

    $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.QSecurityAllowChatTextSelect= mcService.checkRights(mcConst._CMD_.RS.QSecurityAllowChatTextSelect);


        $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 mcToolbar bbsToolbar", 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 mcPopup bbsMenu",
            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  = $rootScope.chatAliases.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($rootScope.chatAliases.BBS, [{
            key   : mcConst.keyCodes.enter,
            func  : $scope.sendText
        }, {
            key   : mcConst.keyCodes.esc,
            func  : $scope.closeBBS
        }], $scope.enterBBSText.getNode());

        $rootScope.hotKeyDispatcher.addPreset($rootScope.chatAliases.BBS + "global", [{
            key   : mcConst.keyCodes.n,
            altKey: true,
            func  : $scope.addNewBBS
        }], document);
    };

    $scope.removeHotKeys = function () {
        $rootScope.hotKeyDispatcher.removePreset($rootScope.chatAliases.BBS);
        $rootScope.hotKeyDispatcher.removePreset($rootScope.chatAliases.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 = $rootScope.chatAliases.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($rootScope.chatAliases.Kanban, [{
        //     key   : mcConst.keyCodes.enter,
        //     func  : function () {
        //     }
        // }], document);
    };

    $scope.removeHotKeys = function () {
        // $rootScope.hotKeyDispatcher.removePreset($rootScope.chatAliases.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 mcUserProfile",
        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        = $rootScope.chatAliases.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 _requestGen = 0;

    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.close = function () {
        if (_view && _view.isVisible()){
            _view.hide();

            $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{}]);
        }
    };

    $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 gen  = ++_requestGen;
        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) {
                    if (gen !== _requestGen) return;

                    $scope.show();

                    $scope.drawFoto(foto);
                }
            ]);
        });

        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='https://api.hackertarget.com/ipgeo/?q=" + 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(){
        if (_view && _view.isVisible()) {
            _view.hide();
        }
    });

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = args[0];

        $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 = $rootScope.chatAliases.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($rootScope.chatAliases.Admin, [{
        //     key   : mcConst.keyCodes.enter,
        //     func  : function () {
        //     }
        // }], document);
    };

    $scope.removeHotKeys = function () {
        // $rootScope.hotKeyDispatcher.removePreset($rootScope.chatAliases.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 = $rootScope.chatAliases.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($rootScope.chatAliases.Admin, [{
        //     key   : mcConst.keyCodes.enter,
        //     func  : function () {
        //     }
        // }], document);
    };

    $scope.removeHotKeys = function () {
        // $rootScope.hotKeyDispatcher.removePreset($rootScope.chatAliases.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 = $rootScope.chatAliases.CreateConf;

    var view         = null;
    var currentQuery = "";
    var findText     = "";
    var currentRes   = "";
    var currentID    = 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(window._messages_.dialogsList.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) {
                $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{}]);
            }
        }
    };

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

    $scope.$on('show' + $scope.Name, function(e, args){
        $scope.container = mcConst.containers.center;

        $scope.show();
    });
}

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 : mcService.insertIco("fa-star-o toolIcon") + mcLang(719), // "719":"Важное",
                    height: 38,
                    css   : "borderBottom byCenter lHeight36 toUpper"
                },

                { 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 = $rootScope.chatAliases.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($rootScope.chatAliases.Admin, [{
        //     key   : mcConst.keyCodes.enter,
        //     func  : function () {
        //     }
        // }], document);
    };

    $scope.removeHotKeys = function () {
        // $rootScope.hotKeyDispatcher.removePreset($rootScope.chatAliases.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,
            css : "mcTerminal",
            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 = $rootScope.chatAliases.Terminal;

    var view        = null;
    var _terminal   = null;

    function init(){
        if (!_terminal) {
            var _isDark = document.body.classList.contains('dark');

            _terminal = new mcTerminalEngine({
                prefix            : mcConst.UserInfo.InternalNick + " >> ",
                $scope            : $scope,
                colorCursor       : _isDark ? '#4ecca3' : 'green',
                colorCursorBg     : _isDark ? '#1c3a4a' : '#3e3e3e',
                multiWordPrefixes : ["traffic", "hotkeys"],
                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({
                            ok    : mcLang(43),
                            cancel: mcLang(44),
                            type  : "confirm-error",
                            text  : mcLang(745),
                            width : 310,
                            callback: function (result) {
                                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 () {
                        this.terminal.innerHTML = "";
                    },

                    "cls": function () {
                        this.terminal.innerHTML = "";
                    },

                    "deletelocalusersdb": function () {
                        webix.confirm({
                            ok    : mcLang(43),
                            cancel: mcLang(44),
                            type  : "confirm-error",
                            text  : "Do you want to remove the local users DB and reload client?",
                            width : 310,
                            callback: function (result) {
                                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),
                            cancel: mcLang(44),
                            type  : "confirm-error",
                            text  : "Soft reboot the client?",
                            width : 310,
                            callback: function (result) {
                                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();
                    }
                },
                onUnknownCommand: function (cmd) {
                    this.addTerminalLine("unknown command: " + cmd);
                }
            });

            // remoteCommands залишаються в контролері — не є частиною ядра
            _terminal.remoteCommands = {
                "ping": function (data) {
                    return [data.Result + " " + ((new Date()).getTime() - data.TimeStamp) + " ms"];
                }
            };

            // traffic-логіка — бізнес-специфіка чату, не переїжджає в ядро
            _terminal.viewTraffic = mcConst.traffic_type.off;

            _terminal.enableTrafficView = function (_type) {
                $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];

                        _terminal.addTerminalLine((inTraf ? ' IN  ' : ' OUT ') + log.replace(mcConst.SessionID, ""));

                        $scope.terminalWrapper.getNode().scrollTo(0, _terminal.wrapper.offsetTop);
                    });
                }

                this.viewTraffic = _type;
            };

            _terminal.disableTrafficView = function () {
                $rootScope.$off("traffic_log");
            };
        }
    }

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

    $scope.regShowWindow = function(id, link){
        // $rootScope.$broadcast('addShowedWindow', [id, link]);
    };

    $scope.focusToTerminal = function(){
        _terminal.blinkCursor();
    };

    $scope.createTerminal = function () {
        _terminal.createTerminal();
    };

    $scope.getTemplate = function () {
        return _terminal.getTemplate();
    };

    $scope.close = function () {
        $rootScope.$broadcast(window._messages_.chatWrapper.setChatViewAndTools, [{}]);
    };
    
    $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];

        $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",
        css    : "mcToolbar sidebarToolbar",
        padding: 0,
        width  : 38,
        rows   : [
            {  id: "btn_" + $scope.aliases.CommonContacts,   view: "toggle", type: "icon", icon: "sitemap", width: 34, css: "iconNoPadding",
                tooltip: mcLang(10),  on: {onItemClick: $scope.clickToolBtn} //  "10" :"Общие контакты",
            },
            {  id: "btn_" + $scope.aliases.PersonalContacts, view: "toggle", type: "icon", icon: "users",   width: 34, css: "iconNoPadding",
                tooltip: mcLang(17),  on: {onItemClick: $scope.clickToolBtn} // "17" :"Личные контакты",
            },
            {  id: "btn_" + $scope.aliases.HistoryDialogs,   view: "toggle", type: "icon", icon: "history",width: 34, css: "iconNoPadding",
                tooltip: mcLang(578), on: {onItemClick: $scope.clickToolBtn} // "578":"История диалогов",
            },

            {  id: "btn_" + $scope.aliases.PrivateInfo,      view: "toggle", type: "icon", icon: "comment", width: 34, css: "iconNoPadding", hidden: true,
                tooltip: mcLang(46),  on: {onItemClick: $scope.clickToolBtn} // "46" :"Приватный разговор",
            },
            {  id: "btn_" + $scope.aliases.ConfUserList,     view: "toggle", type: "icon", icon: "comments",width: 34, css: "iconNoPadding", hidden: true,
                tooltip: mcLang(26),  on: {onItemClick: $scope.clickToolBtn} // "26" :"Люди в конференции",
            },
            {  id: "btn_" + $scope.aliases.MediaConfUsers,  view: "toggle", type: "icon", icon: "headphones", width: 34, css: "iconNoPadding", hidden: true,
                tooltip: mcLang(1701), on: {onItemClick: $scope.clickToolBtn} // "1701":"Учасники дзвінка",
            },
            {  id: "btn_" + $scope.aliases.CommonFiles,      view: "toggle", label: "FTP", width: 34, css: "iconNoPadding font95All", hidden: $scope.isWeb,
                tooltip: mcLang(658), on: {onItemClick: $scope.clickToolBtn} // "658":"FTP файлы",
            },
            {  id: "btn_" + $scope.aliases.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_" + $scope.aliases.CommonContacts);
    $scope.toolBtn.PersonalContacts = $$("btn_" + $scope.aliases.PersonalContacts);
    $scope.toolBtn.PrivateInfo      = $$("btn_" + $scope.aliases.PrivateInfo);
    $scope.toolBtn.ConfUserList     = $$("btn_" + $scope.aliases.ConfUserList);
    $scope.toolBtn.MediaConfUsers   = $$("btn_" + $scope.aliases.MediaConfUsers);
    $scope.toolBtn.CommonFiles      = $$("btn_" + $scope.aliases.CommonFiles);
    $scope.toolBtn.HistoryDialogs   = $$("btn_" + $scope.aliases.HistoryDialogs);
    $scope.toolBtn.ReceiveFiles     = $$("btn_" + $scope.aliases.ReceiveFiles);

    $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             = $rootScope.chatAliases.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.toolBtn.MediaConfUsers  && $scope.toolBtn.MediaConfUsers.setValue(false);
            $rootScope.isWebClient && $scope.toolBtn.CommonFiles.setValue(false);
        }
    };
    $scope.containers       = mcConst.containers;
    $scope.notShowRightSide = false;

    var view                  = null;
    var pinTool               = '';
    var hideRightSideByResize = false;
    var addingWidth           = 0;
    var findText              = "";
    var saveWndSizeTimer      = null;
    var menuShowed            = false;
    $scope.aliases               = $rootScope.chatAliases;

    // === Навігація: константи ===
    var _setToolInfo = function (list, isOpen){
        return { rightToolbar: isOpen || false, rightTools: mcService.isArray(list) ? list : [] }
    };
    var frameConfig = {
        ChatFrame       : _setToolInfo(['PrivateInfo','ConfUserList','MediaConfUsers','CommonContacts',
                                            'PersonalContacts','CommonFiles','HistoryDialogs',
                                            'ReceiveFiles'], true),
        Settings        : _setToolInfo([], false),
        UserProfile     : _setToolInfo([], false),
        HistoryBroadcast: _setToolInfo([], false),
        ViewLogs        : _setToolInfo([], false),
        CreateConf      : _setToolInfo([], false),
        BBS             : _setToolInfo([], false),
        Kanban          : _setToolInfo([], false),
        Admin           : _setToolInfo([], false),
        Forum           : _setToolInfo([], false),
        SendBroadcast   : _setToolInfo([], false),
        Terminal        : _setToolInfo([], false),
        Important       : _setToolInfo([], false)
    };

    var overlayFrames = [
        $scope.aliases.Settings,
        $scope.aliases.Terminal,
        $scope.aliases.ViewLogs,
        $scope.aliases.UserProfile,
        $scope.aliases.CreateConf,
        $scope.aliases.Important
    ];

    // Стан навігації
    var currentFrame  = $scope.aliases.ChatFrame;
    var currentParams = null;
    var currentTool   = null;

    var overlay       = null;  // { frame, params } — якщо відкрито overlay
    var prevState     = null;  // { frame, params, tool } — стан до overlay
    var _rights  = {
        QAllowFindUsersTool     : false,
        QCommonAllowDialogs     : false,
        QAllowContactsListsTotal: false,
        QShowCommonContactsList : false,
        QFTPPublic              : false,
        QFTPPersonal            : false,
        QViewTextConfMembersList: false
    };
    var optionsIsLoaded       = false;
    var CurrentDialog         = {
        id    : '-1',
        type  : $rootScope.chatAliases.UIN,
        alias : $rootScope.chatAliases.PrivateInfo
    };

    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 syncCurrentDialog() {
        if (currentFrame === $scope.aliases.ChatFrame && currentParams && typeof currentParams === 'string') {
            if (currentParams.indexOf('UIN-') === 0) {
                CurrentDialog = {
                    id   : currentParams.slice(4),
                    type : $scope.aliases.UIN,
                    alias: $scope.aliases.PrivateInfo
                };
            } else
            if (currentParams.indexOf('UID-') === 0) {
                CurrentDialog = {
                    id   : currentParams.slice(4),
                    type : $scope.aliases.UID,
                    alias: $scope.aliases.ConfUserList
                };
            } else
            if (currentParams.indexOf('dUIN') === 0) {
                CurrentDialog = {
                    id   : currentParams.slice(5),
                    type : $scope.aliases.UIN,
                    alias: $scope.aliases.PrivateInfo
                };
            } else
            if (currentParams.indexOf('dUID') === 0) {
                CurrentDialog = {
                    id   : currentParams.slice(5),
                    type : $scope.aliases.UID,
                    alias: $scope.aliases.ConfUserList
                };
            }
        } else
        if (currentFrame !== $scope.aliases.ChatFrame) {
            CurrentDialog = {
                id: '0',
                type: currentFrame,
                alias: currentFrame
            };
        }
    }

    function isToolAllowed(tool) {
        switch (tool) {
            case $scope.aliases.CommonContacts:   return _rights.QShowCommonContactsList;
            case $scope.aliases.PersonalContacts: return _rights.QAllowContactsListsTotal;
            case $scope.aliases.ConfUserList:     return _rights.QViewTextConfMembersList;
            case $scope.aliases.CommonFiles:      return (_rights.QFTPPublic || _rights.QFTPPersonal) && !$rootScope.isWebClient;
            default:                              return true;
        }
    }

    function resolveRightTool(frame, params, toolOverride) {
        var cfg        = frameConfig[frame] || _setToolInfo([], false);
        var rightTools = cfg.rightTools;

        if (toolOverride && rightTools.indexOf(toolOverride) !== -1) {
            return toolOverride;
        }
        if (pinTool && rightTools.indexOf(pinTool) !== -1) {
            return pinTool;
        }
        if (params && mcService.isString(params) && params.indexOf('UIN') !== -1 && params !== 'dUIN-0' && params !== 'UIN-0') {
            return $scope.aliases.PrivateInfo;
        }
        if (params && mcService.isString(params) && params.indexOf('UID') !== -1) {
            return $scope.aliases.ConfUserList;
        }
        if (currentTool && rightTools.indexOf(currentTool) !== -1) {
            return currentTool;
        }

        return null;
    }

    function getActiveFrame() {
        return overlay ? overlay.frame : currentFrame;
    }

    function openFrame(frame, params, toolOverride) {
        var cfg = frameConfig[frame] || _setToolInfo([], false);

        // hide поточного фрейму
        $rootScope.$broadcast('hide' + getActiveFrame());

        // оновити стан
        currentFrame  = frame;
        currentParams = params || null;
        overlay       = null;
        prevState     = null;

        // show нового фрейму
        $rootScope.$broadcast('show' + frame, [$scope.containers[frame], currentParams]);

        // оновити виділення в лівій панелі
        if (frame === $scope.aliases.ChatFrame && currentParams) {
            $rootScope.$broadcast(window._messages_.dialogsCtrl.selectDialog, [currentParams]);
        }

        // правий тулбар
        if (cfg.rightToolbar) {
            !mcConst.UserInfo.hasInvite && $scope.rightToolbar.show();
        } else {
            $scope.rightToolbar.hide();
        }

        // правий тул
        var tool = resolveRightTool(frame, currentParams, toolOverride);

        currentTool = tool;

        // оновити кнопки тулбару завжди (PrivateInfo ↔ ConfUserList)
        switchToolBtnVisible(tool, pinTool);

        // завжди бродкастити show для тулу (потрібно для оновлення даних, напр. фото в заголовку)
        if (tool) {
            $rootScope.$broadcast('show' + tool, [$scope.containers[tool]]);
        }

        if (tool && !$scope.notShowRightSide) {
            if (pinTool && tool === pinTool) {
                // pin-тулза: показати без скидання pin
                showRightSide();
            } else {
                selectToolView(tool);
            }
        } else if (!pinTool) {
            hideRightSide();
        }

        syncCurrentDialog();
    }

    function openOverlay(frame, params) {
        // зберегти стан
        prevState = { frame: currentFrame, params: currentParams, tool: currentTool };

        // hide поточного фрейму (або попереднього overlay)
        $rootScope.$broadcast('hide' + getActiveFrame());

        // відкрити overlay
        overlay = { frame: frame, params: params || null };

        $rootScope.$broadcast('show' + frame, [$scope.containers[frame], overlay.params]);

        // сховати правий тулбар
        $scope.rightToolbar.hide();
        hideRightSide();
    }

    function closeOverlay() {
        if (!overlay) return;

        // hide overlay
        $rootScope.$broadcast('hide' + overlay.frame);
        overlay = null;

        if (prevState) {
            // відновити стан
            currentFrame  = prevState.frame;
            currentParams = prevState.params;
            currentTool   = prevState.tool;
            prevState     = null;

            // show відновленого фрейму
            $rootScope.$broadcast('show' + currentFrame, [$scope.containers[currentFrame], currentParams]);

            // відновити виділення в лівій панелі
            if (currentFrame === $scope.aliases.ChatFrame && currentParams) {
                $rootScope.$broadcast(window._messages_.dialogsCtrl.selectDialog, [currentParams]);
            }

            // відновити правий тулбар та тул
            var cfg = frameConfig[currentFrame] || _setToolInfo([], false);

            if (cfg.rightToolbar) {
                !mcConst.UserInfo.hasInvite && $scope.rightToolbar.show();
            }

            if (currentTool && !$scope.notShowRightSide) {
                $scope.changeTool(currentTool);
                showRightSide();
            } else
            if (!pinTool) {
                hideRightSide();
            }

            syncCurrentDialog();
        }
    }

    // === /Навігація ===

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

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

    // === Set Chat View Type ========================

    $rootScope.SetPrivate = function(UIN){ // PRIVATE
        CurrentDialog = {
            id: UIN, type: $rootScope.chatAliases.UIN, alias: $rootScope.chatAliases.PrivateInfo
        };
    };

    $rootScope.SetKanban  = function(){ // KANBAN
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.Kanban, alias: $rootScope.chatAliases.Kanban
        };
    };

    $rootScope.SetForum  = function(){
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.Forum, alias: $rootScope.chatAliases.Forum
        };
    };

    $rootScope.SetAdmin  = function(){
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.Admin, alias: $rootScope.chatAliases.Admin
        };
    };

    $rootScope.SetSendBroadcast = function(){ //
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.SendBroadcast, alias: $rootScope.chatAliases.SendBroadcast
        };
    };

    $rootScope.SetHistoryBroadcast = function(){ //
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.HistoryBroadcast, alias: $rootScope.chatAliases.HistoryBroadcast
        };
    };

    $rootScope.SetBBS     = function(){ // BBS
        CurrentDialog = {
            id: '0', type: $rootScope.chatAliases.BBS, alias: $rootScope.chatAliases.BBS
        };
    };

    $rootScope.SetConf    = function(UID){ // CHANNEL
        CurrentDialog = {
            id: UID, type: $rootScope.chatAliases.UID, alias: $rootScope.chatAliases.ConfUserList
        };
    };

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

    function unPinTool(noSave){
        pinTool = '';

        if (currentTool && currentTool === pinTool) {
            currentTool = null;
        }

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

    }
    
    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 $rootScope.chatAliases.PrivateInfo:
                if ($rootScope.GetChatTypeID() === $rootScope.chatAliases.Elisa){
                    $scope.toolBtn.PrivateInfo.hide();
                } else {
                    $scope.toolBtn.PrivateInfo.show();
                }
                
                $scope.toolBtn.ConfUserList.hide();

                paintBG('privateBg');
            break;

            case $rootScope.chatAliases.ConfUserList:
                $scope.toolBtn.PrivateInfo.hide();

                _rights.QViewTextConfMembersList && $scope.toolBtn.ConfUserList.show();

                paintBG('bgMC');
            break;

            case $rootScope.chatAliases.CommonContacts:
            case $rootScope.chatAliases.HistoryDialogs:
            case $rootScope.chatAliases.PersonalContacts:
            case $rootScope.chatAliases.CommonFiles:
            case $rootScope.chatAliases.MediaConfUsers:
                // no action
            break;

            case $rootScope.chatAliases.ReceiveFiles:
                $scope.toolBtn[$rootScope.chatAliases.ReceiveFiles].show();
            break;

            default:
                if (!currentTool) {
                    if ($scope.toolBtn.PrivateInfo.isVisible())  currentTool = $rootScope.chatAliases.PrivateInfo;
                    if ($scope.toolBtn.ConfUserList.isVisible()) currentTool = $rootScope.chatAliases.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[$rootScope.chatAliases.CommonContacts]   = mcConst.containers.right;
        $scope.containers[$rootScope.chatAliases.PersonalContacts] = mcConst.containers.right;
        $scope.containers[$rootScope.chatAliases.PrivateInfo]      = mcConst.containers.right;
        $scope.containers[$rootScope.chatAliases.ConfUserList]     = mcConst.containers.right;
        $scope.containers[$rootScope.chatAliases.MediaConfUsers]   = mcConst.containers.right;
        $scope.containers[$rootScope.chatAliases.CommonFiles]      = mcConst.containers.right;
        $scope.containers[$rootScope.chatAliases.HistoryDialogs]   = mcConst.containers.right;
        $scope.containers[$rootScope.chatAliases.ReceiveFiles]     = mcConst.containers.right;

        $scope.containers[$rootScope.chatAliases.Dialogs]          = mcConst.containers.left;
        $scope.containers[$rootScope.chatAliases.MainMenu]         = mcConst.containers.left;

        $scope.containers[$rootScope.chatAliases.ChatFrame]        = mcConst.containers.center;
        $scope.containers[$rootScope.chatAliases.UserProfile]      = mcConst.containers.center;
        $scope.containers[$rootScope.chatAliases.BBS]              = mcConst.containers.center;
        $scope.containers[$rootScope.chatAliases.Kanban]           = mcConst.containers.center;
        $scope.containers[$rootScope.chatAliases.ViewLogs]         = mcConst.containers.center;
        $scope.containers[$rootScope.chatAliases.Settings]         = mcConst.containers.center;
        $scope.containers[$rootScope.chatAliases.Terminal]         = mcConst.containers.center;
        $scope.containers[$rootScope.chatAliases.SendBroadcast]    = mcConst.containers.center;
        $scope.containers[$rootScope.chatAliases.Admin]            = mcConst.containers.center;
        $scope.containers[$rootScope.chatAliases.Forum]            = mcConst.containers.center;
        $scope.containers[$rootScope.chatAliases.HistoryBroadcast] = mcConst.containers.center;
        $scope.containers[$rootScope.chatAliases.Important]        = mcConst.containers.center;

        data[$rootScope.chatAliases.CommonContacts  ] = mcService.Lang(10);  //"10" : "Контакты",
        data[$rootScope.chatAliases.PersonalContacts] = mcService.Lang(17);  //"17" : "Личные контакты",
        data[$rootScope.chatAliases.PrivateInfo     ] = mcService.Lang(46);  //"46" : "Приватный разговор",
        data[$rootScope.chatAliases.ConfUserList    ] = mcService.Lang(26);   //"26" : "Люди в конференции",
        data[$rootScope.chatAliases.MediaConfUsers  ] = mcService.Lang(1701); //"1701":"Учасники дзвінка",
        data[$rootScope.chatAliases.CommonFiles     ] = mcService.Lang(567);  //"567":"Общие файлы на сервере",
        data[$rootScope.chatAliases.HistoryDialogs  ] = mcService.Lang(578); //"578":"История диалогов",
        data[$rootScope.chatAliases.ReceiveFiles    ] = mcService.Lang(588); //"588":"Прием файлов",

        selectToolList.defineData(data);
        selectToolList.onChange = $scope.changeTool;
    }

    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 $rootScope.chatAliases.ReceiveFiles:
                if ($scope.toolBtn[tool].isVisible()){
                    $scope.toolBtn[tool].setValue(false);
                    $scope.toolBtn[tool].hide();

                    if (selectToolList.getValue() === tool){
                        if ($rootScope.GetChatType() === $rootScope.chatAliases.UIN && $rootScope.GetChatTypeID() !== $rootScope.chatAliases.Elisa){
                            $scope.changeTool($rootScope.chatAliases.PrivateInfo);
                        } else
                        if ($rootScope.GetChatType() === $rootScope.chatAliases.UID){
                            $scope.changeTool($rootScope.chatAliases.ConfUserList);
                        } else {
                            hideRightSide();

                            currentTool = selectToolList.getPrevValue();
                        }
                    }
                }
            break;
        }
    }

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

    $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" + $rootScope.chatAliases.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' + $rootScope.chatAliases.Dialogs);
            $rootScope.$broadcast(window._messages_.search.searchUsers, [findText]);
        } else
        if (newFindText === ""){
            findText = "";
            
            $rootScope.$broadcast('hide' + $rootScope.chatAliases.Search);
            $rootScope.$broadcast('show' + $rootScope.chatAliases.Dialogs, [$scope.containers[$rootScope.chatAliases.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' + $scope.aliases.Dialogs);
            $rootScope.$broadcast('show' + $scope.aliases.MainMenu, [$scope.containers[$scope.aliases.MainMenu]]);

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

            // закрити overlay, якщо він був відкритий з меню
            if (overlay) {
                closeOverlay();
            }

            $rootScope.$broadcast('hide' + $scope.aliases.MainMenu);
            $rootScope.$broadcast('show' + $scope.aliases.Dialogs, [$scope.containers[$scope.aliases.Dialogs]]);
        }
    };

    $scope.changeTool = function(tool, oldTool){
        var currentType = $rootScope.GetChatType();

        oldTool = oldTool || selectToolList.getValue();

        if ($rootScope.GetChatTypeID() === $rootScope.chatAliases.Elisa && tool === $rootScope.chatAliases.PrivateInfo){
            switchToolBtnVisible();
        }

        if (($rootScope.GetChatTypeID() === $rootScope.chatAliases.Elisa && tool === $rootScope.chatAliases.PrivateInfo) ||
            (tool === $rootScope.chatAliases.ConfUserList && currentType === $rootScope.chatAliases.UIN) ||
            (tool === $rootScope.chatAliases.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(){
        $scope.toolBtn.offAll();

        if (this && this.setValue){
            this.setValue(true);

            currentTool = this.config.id.split('_')[1];
        }
    };

    $scope.clickToolBtn = function(){
        var oldTool = selectToolList.getValue();
        var newTool = this.config.id.split('_')[1];

        if (oldTool === newTool && !this.getValue()){
            hideRightSide();

            $rootScope.$broadcast('hide' + oldTool);

            $scope.notShowRightSide = true;

            currentTool = null;
        } else {
            $scope.notShowRightSide = false;

            $scope.changeTool(newTool);

            currentTool = newTool;
        }

        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" class="__googleLink">' +
                                            '        <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,
                                        text         : "https://mychat-server.com/?id=" + servID + "&o=" + opt,
                                        width        : w_9,
                                        height       : h_9,
                                        colorDark    : "#131313",
                                        colorLight   : "#ffffff",
                                        correctLevel : QRCode.CorrectLevel.L
                                    });

                                    var link = mcService.getElementByClass("__googleLink")[0];
                                    if (link){
                                        link.href += "&referrer=" + opt;
                                    }
                                }
                            }
                        });

                        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    : 400,
            css      : "mcWin",
            id       : "checkPwdWnd",
            head     : { cols:[
                { width: 42 },
                { view : "label",  label:`<div class="byCenter">${mcLang(718)}</div>`}, // "718":"QRCode для подключения с телефона",
                { view : "button", type: 'icon', width: 42, icon: 'times', css: "button_transparent", click: function () {
                    closePwd();
                } }
            ]},
            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",
        setChatViewAndTools : "setChatViewAndTools",
        toggleTool          : "toggleTool",
        pinTool             : "pinTool",
        unPinTool           : "unPinTool",
        hideTool            : "hideTool",
        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('conf_status_change', function (e, args) {
        var info = args[0];

        if (!$scope.toolBtn.MediaConfUsers) return;

        if (info && info.State > 0) {
            $scope.toolBtn.MediaConfUsers.show();
        } else {
            $scope.toolBtn.MediaConfUsers.setValue(false);
            $scope.toolBtn.MediaConfUsers.hide();

            if (selectToolList.getValue() === $rootScope.chatAliases.MediaConfUsers) {
                var chatType = $rootScope.GetChatType();

                if (chatType === $rootScope.chatAliases.UID) {
                    $scope.changeTool($rootScope.chatAliases.ConfUserList);
                } else if (chatType === $rootScope.chatAliases.UIN) {
                    $scope.changeTool($rootScope.chatAliases.PrivateInfo);
                }
            }
        }
    });

    $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.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(_msg.setChatViewAndTools, function(e, args){
        var opts = args[0] || {};

        if (!opts.frame) {
            // закрити overlay
            closeOverlay();
        } else
        if (overlayFrames.indexOf(opts.frame) !== -1) {
            openOverlay(opts.frame, opts.params || null);
        } else {
            // спочатку заповнюємо фрейм, виконується призначення frameID = $rootScope.GetChatTypeID();
            if (opts.fillArgs) {
                $rootScope.$broadcast(window._messages_.chatFrame.fillChatFrame, opts.fillArgs);
            }
            // тільки після цього - показуємо його
            openFrame(opts.frame, opts.params || null, opts.tool || null);
        }
    });

    $scope.$on(_msg.toggleTool, function(e, args) {
        var tool = args ? args[0] : undefined;

        if (tool) {
            // відкрити конкретну тулзу з перевіркою
            var cfg = frameConfig[currentFrame];

            if (!cfg || !cfg.rightToolbar ||
                cfg.rightTools.indexOf(tool) === -1 ||
                !isToolAllowed(tool)) return;

            $scope.notShowRightSide = false;
            $scope.changeTool(tool);
            showRightSide();

            currentTool = tool;
        } else {
            // toggle поточної тулзи
            if ($scope.rightSide.isVisible()) {
                hideRightSide();

                if (currentTool) $rootScope.$broadcast('hide' + currentTool);

                $scope.notShowRightSide = true;
            } else
            if (currentTool) {
                $scope.changeTool(currentTool);
                showRightSide();
                $scope.notShowRightSide = false;
            }

            optionsIsLoaded && $rootScope.Storage.options.save({
                notShowRightSide: $scope.notShowRightSide
            });
        }
    });

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

        // Ініціалізуємо стан навігації
        currentFrame  = $scope.aliases.ChatFrame;
        currentParams = null;
        currentTool   = null;
        overlay       = null;
        prevState     = null;

        $rootScope.$broadcast('show' + $rootScope.chatAliases.Dialogs, [$scope.containers[$rootScope.chatAliases.Dialogs]]);

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

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

    $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);
        _rights.QSecurityAllowInterfaceScreenShots = mcService.checkRights(mcConst._CMD_.RS.QSecurityAllowInterfaceScreenShots);
        _rights.QSecurityAllowChatTextSelect       = mcService.checkRights(mcConst._CMD_.RS.QSecurityAllowChatTextSelect);

        $rootScope.$broadcast('sendCMDToElectron', [
            mcConst._CMD_.ce_content_protection,
            _rights.QSecurityAllowInterfaceScreenShots
        ]);

        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' + $rootScope.chatAliases.PersonalContacts);
        !_rights.QAllowContactsListsTotal && $rootScope.$broadcast('hide' + $rootScope.chatAliases.CommonContacts);
        !_rights.QViewTextConfMembersList && $rootScope.$broadcast('hide' + $rootScope.chatAliases.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
                });
            }
        });

        ['copy', 'cut'].forEach((eventName) => {
            document.addEventListener(eventName, (e) => {
                if (!_rights.QSecurityAllowChatTextSelect) {
                    e.preventDefault();

                    console.warn("Copying text was disabled by rights set");
                } else {

                }
            });
        });
    });
}



﻿function mainController($scope, $rootScope, mcCmdParser, mcConnect,/*mcCmdLoop,*/ $http, $location, mcStatistics, mcEmojiList){
    "use strict";

    function initLockWindow(closeBtn, cb){
        if (lockWindow){
            lockWindow.destructor();
        }

        lockWindow = webix.ui({
            view     : "window",
            modal    : true,
            css      : "mcWin",
            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 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";
        }
    }

    // === SEND & RECEIVE ================================================

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

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

    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.needFoto          = false;
    $rootScope.Purl              = purl();
    $rootScope.smileSetNumb      = "1";
    $rootScope.wndSize           = mcService.getWindowSize();
    $rootScope.isWebClient       = location.protocol !== 'file:';
    $rootScope.SendCMDToServer   = sendCMD;
    $rootScope.ProcessCMD        = receiveCMD;
    $rootScope.dropConnectOnLogin= false;
    $rootScope.chatAliases       = {
        // Назви компонентів (раніше mcConst.dataModels)
        Login            : 'Login',
        Main             : 'Main',
        ChatWrapper      : 'ChatWrapper',
        ChatFrame        : 'ChatFrame',
        ConfUserList     : 'ConfUserList',
        MediaConfUsers   : 'MediaConfUsers',
        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',
        // Спеціальні аліаси для типів діалогів
        Elisa : 'UIN-0',
        Conf  : 'ConfUserList',
        UIN   : 'UIN',
        UID   : 'UID',
        Menu  : 'Menu',
        Privat: 'PrivateInfo'
    };
    $rootScope.mcStorage         = mcService.Marge({}, $rootScope.chatAliases);

    // 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' + $rootScope.chatAliases.Login, []);
        }
    };

    $rootScope.ErrorActions[11] = $rootScope.ErrorActions[17];

    // ====================================================
    $rootScope.getDisplayName = function (uin) {
        return $rootScope.$broadcast(window._messages_.usersSmallInfo.getUserSmallInfo, [uin]).DisplayName || ""
    };

    $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(window._messages_.dialogsList.OpenPrivate, [uin]);
        }
    };

    $rootScope.initSocketIo      = function () {
        if (window.hasOwnProperty("io") && mcService.isFunction(window.io) && !mcConst.socketIO){
            mcConst.socketIO = io({
                transports: mcConst.io_transport,
                'reconnection': true,
                'reconnectionDelay': 1000,
                'reconnectionDelayMax' : 5000,
                'reconnectionAttempts': 5,
                'timeout' : 120000
            });

            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._messageActions = {};
    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.setChatViewAndTools, [{ frame: $rootScope.chatAliases.UserProfile, params: uin }]);
        }
    };
    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 mx, my;
    
    function onMouseUpdate(e) {
        mx = e.pageX;
        my = e.pageY;
    }

    document.addEventListener('mousemove', onMouseUpdate, false);
    document.addEventListener('mouseenter', onMouseUpdate, false);

    $rootScope.getMouseX = function() {
        return mx;
    }

    $rootScope.getMouseY = function() {
        return my;
    }

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

    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, function _startPingTimer (){
        if (!mcConst.PingTimer)
            mcConst.PingTimer = mcConnect.StartPingTimer(mcConst.PingInterval, {
                onReconnect: function () {
                    $rootScope.SendCMDToServer([mcConst._CMD_.Ping, mcConst.SessionID]);
                },
                onError : function(err){
                    if ($rootScope.isWebClient){
                        mcConst.LoggedIn = false;

                        if (!(mcConst.ClientSettings.LogsPrivates && mcConst.ClientSettings.LogsTxtChannels)) {
                            $rootScope.$broadcast('hide' + $rootScope.chatAliases.ChatWrapper, [true]);
                        }

                        if ($rootScope.loginFormHidden) {
                            $rootScope.$broadcast('ErrorMsg', [mcConst._CMD_.Errors.Disconnected, mcService.Error(mcConst._CMD_.Errors.Disconnected), '', function(){ // "223": "потеряно соединение с сервером",
                                location.search = "";
                                location.reload();
                            }]);
                        }
                    } else {
                        $rootScope.restoreConnection = false;

                        $rootScope.$broadcast('ReconnectLoop');

                        $rootScope.$broadcast('ErrorMsg', [mcConst._CMD_.Errors.Disconnected, mcService.Error(mcConst._CMD_.Errors.Disconnected) + "<br>" + mcLang(539) + "<br><br>" + mcConst.loadGif, '', function(){ // "539":"Восстанавливаем связь!",
                            mcService.ClearSessionSettings(true, true);

                            location.search = "";
                            location.reload();
                        }, mcLang(540)]); // "540":"Прервать",
                    }

                    if (mcCmdParser.runCmd) {
                        mcCmdParser.runCmd(false, [mcConst._CMD_.sc_error, {
                            "ErrNum": mcConst._CMD_.Errors.NetworkError,
                            "Params": [err || ""]
                        }]);
                    }
                },
                onStop: function () {
                    if ($rootScope.isWebClient && mcConst.LoggedIn && mcCmdParser.runCmd) {
                        $rootScope.$broadcast('ErrorMsg', [mcConst._CMD_.Errors.Disconnected, mcService.Error(mcConst._CMD_.Errors.Disconnected), '', function(){ // "223": "потеряно соединение с сервером",
                            location.search = "";
                            location.reload();
                        }]);
                    }

                    mcConst.LoggedIn  = false;
                    mcConst.PingTimer = null;
                    mcConst.SessionID = '';

                    $rootScope.destroySocketIo();

                    if (!$rootScope.isWebClient){
                        $rootScope.$broadcast("disableGlobalHotKeys");
                    }
                }
            });
    });

    $scope.$on(_msg.StopPingTimer, function(e, args){
        if (mcConst.PingTimer) {
            if (mcConst.LoggedIn && mcStatistics && mcStatistics.saveStat){
                mcStatistics.saveStat(mcConst.ServerInfo.ID, mcConst.UserInfo.UIN);
            }

            mcConnect.StopPingTimer.apply(null, args);

            mcConst.PingTimer = null;
        }
    });

    $scope.$on(_msg.ReconnectLoop, function () {
        if (!mcConst.PingTimer && !$rootScope.restoreConnection && $rootScope.loginFormHidden && !$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;

            $http(MC_RESOURCE.TextSource.errors.client + lang + '.errors.json').success(function(txt){
                mcConst.ErrorText = mcService.isObject(txt) ? txt : JSON.parse(txt);
            });

            mcService.loadTextSource($http, "admin");
            mcService.loadTextSource($http, "common");
            mcService.loadTextSource($http, window.__projectName__, function () {
                if ($rootScope.isWebClient){
                    $scope.$broadcast('show' + $rootScope.chatAliases.Login, [lang]);
                } else {
                    $scope.prepareActions(lang);
                }

                $rootScope.isWebClient && mcService.CheckNotification();

                webix.i18n.setLocale(lang);
            });
        }).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);

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

    var _savedLang = mcService.storage.get(mcConst.storageFields.lang);

    $scope.$emit('ChangLanguage', mcService.detectLang(_savedLang));

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

    mcEmojiList.load();

    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) {
    window.__projectName__ = 'chat';

    var units   = []
        // .concat(location.protocol === 'file:' ? { module: 'cLog', args: []} : [])
        .concat({ module: 'mcPlaySound',      args: ['$rootScope']})
        .concat({ module: 'mcConnect',        args: ['$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', 'mcEmojiList']})

        .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: 'mediaConfUsersController',   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(window.__projectName__);
    window.mcLang    = mcService.Lang;

    mcComponents.units(units).start();
}

// Функція для генерації ключа фіксованої довжини
function ___getKey(key) {
    return CryptoJS.SHA256(key);
}

// Функція шифрування
function ___encrypt(text, key) {
    const iv = CryptoJS.lib.WordArray.random(16); // Генерація випадкового IV довжиною 16 байтів
    // const iv = CryptoJS.enc.Hex.parse('00000000000000000000000000000000');
    const fixedKey = ___getKey(key); // Генерація фіксованого ключа (32 байти)
    const encrypted = CryptoJS.AES.encrypt(text, fixedKey, {
        iv: iv,
        mode: CryptoJS.mode.CTR,
        padding: CryptoJS.pad.NoPadding//Pkcs7 // Використання стандартного паддінгу
    });
    const ivCiphertext = iv.concat(encrypted.ciphertext); // Конкатенація IV та зашифрованого тексту
    return CryptoJS.enc.Base64.stringify(ivCiphertext); // Перетворення у формат Base64
}

// Функція дешифрування
function ___decrypt(encryptedText, key) {
    const ivCiphertext = CryptoJS.enc.Base64.parse(encryptedText); // Перетворення з Base64

    // Розмір IV у байтах та словах
    const ivSize = 16; // 16 байтів для IV
    const ivWords = ivSize / 4; // 4 слова (1 слово = 4 байти)

    // Витягування IV з першої частини даних
    const iv = CryptoJS.lib.WordArray.create(ivCiphertext.words.slice(0, ivWords), ivSize);

    // Витягування зашифрованого тексту з залишку
    const ciphertextBytes = ivCiphertext.sigBytes - ivSize;
    const ciphertext = CryptoJS.lib.WordArray.create(ivCiphertext.words.slice(ivWords), ciphertextBytes);

    const fixedKey = ___getKey(key); // Генерація фіксованого ключа (32 байти)
    const decrypted = CryptoJS.AES.decrypt(
        { ciphertext: ciphertext },
        fixedKey,
        {
            iv: iv,
            mode: CryptoJS.mode.CTR,
            padding: CryptoJS.pad.NoPadding//Pkcs7 // Використання такого ж паддінгу, як і при шифруванні
        }
    );
    return CryptoJS.enc.Base64.parse(decrypted).toString(CryptoJS.enc.Utf8);
}

/*
// Приклад використання
const key = 'Приклад використання'; // Ваш ключ шифрування будь-якої довжини
var text = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Utf8.parse('аш ключ шифрування будь-якої довжини'));

const encryptedText = ___encrypt(text, key);
console.log('Зашифрований текст:', encryptedText);

const decryptedText = ___decrypt(encryptedText, key);
console.log('Розшифрований текст:', decryptedText);

*/

