Greasy Fork is available in English.

Nekto.me - Быстрый переход к новому диалогу

Nekto.me: добавляет кнопки для быстрого перехода к новому диалогу

// ==UserScript==
// @name      Nekto.me - Быстрый переход к новому диалогу
// @namespace    http://nekto.me
// @version      0.35
// @description   Nekto.me: добавляет кнопки для быстрого перехода к новому диалогу
// @author       Krita
// @match        http://nekto.me/chat*
// @match        https://nekto.me/chat*
// @grant       GM_addStyle
// @grant       GM_getResourceText

// ==/UserScript==


GM_addStyle ( `
.checkbox, .checkbox input[type="checkbox"]{
margin: 0
}
.checkbox label{
padding: 0;
margin-left: 20px
}
.night_theme .dropdown-menu{
background-color: #101417;
}
.night_theme .dropdown-menu > li > a {
color: #e2e3e7;
}
.dropdown-menu li.checkbox{
display: inline-flex;
margin: 0px 6px;
align-items: center;
}
.right_block_hc.main_chat_but{
display: flex;
}
button.btn.btn-md.btn-my1{
border-radius: 50px !important;
}
.btn-group {
    margin-left: 6px;
}

.progress-countdown{
  height: 8px;
  margin-bottom: -8px;
  position: relative;
  background-color: transparent;
}

.progress-countdown .progress-bar{
  animation: progressbar-countdown;
  animation-iteration-count: 1;
  animation-fill-mode: forwards;
  animation-play-state: paused;
  animation-timing-function: linear;
}
@keyframes progressbar-countdown {
  0% {
    width: 100%;
    background: #3bb93b;
  }
  100% {
    width: 0%;
    background: #1e94d4;
  }
}

`);

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

const options = {
    autoDialog: true,   // Автоматически переходить к новому диалогу
    skipNoAnswer: true, // Пропускать собеседников, которые не отвечают
    skipFilter: false,  // Пропускать сообщения, совпадающие с фиильтром
    skipDelay: 20,      // Задержка в секундах перед пропуском
    filterCount: 2,     // Количество сообщений, проверяемых фильтром
    maxNoSkipCount: 50, // Количество сообщений, после которых все галочки снимаются
    // Удалите знак комментария "//" перед нужными вам фильтрами
    // Фильтры задаются в виде регулярных выражений
    filter: [
        {
            name: "Нецензурная лексика",
            regexp: /(?<=(^|[^а-я]))((у|[нз]а|(хитро|не)?вз?[ыьъ]|с[ьъ]|(и|ра)[зс]ъ?|(о[тб]|под)[ьъ]?|(.\B)+?[оаеи])?-?([её]б(?!о[рй])|и[пб][ае][тц]).*?|(н[иеа]|([дп]|верт)о|ра[зс]|з?а|с(ме)?|о(т|дно)?|апч)?-?ху([яйиеёю]|ли(?!ган)).*?|(в[зы]|(три|два|четыре)жды|(н|сук)а)?-?бл(я(?!(х|ш[кн]|мб)[ауеыио]).*?|[еэ][дт]ь?)|(ра[сз]|[зн]а|[со]|вы?|п(ере|р[оие]|од)|и[зс]ъ?|[ао]т)?п[иеё]зд.*?|(за)?п[ие]д[аое]?р(ну.*?|[оа]м|(ас)?(и(ли)?[нщктл]ь?)?|(о(ч[еи])?|ас)?к(ой)|юг)[ауеы]?|манд([ауеыи](л(и[сзщ])?[ауеиы])?|ой|[ао]вошь?(е?к[ауе])?|юк(ов|[ауи])?)|муд([яаио].*?|е?н([ьюия]|ей))|мля([тд]ь)?|лять|([нз]а|по)х|м[ао]л[ао]фь([яию]|[еёо]й))(?=($|[^а-я]))/img
        },
        //{
        //    name: "Только строчные или прописные",
        //    regexp: /^[А-Я\s]+$|^[а-я\s]+$/gm
        //},
        //{
        //    name: "Предложения перейти в месседжеры",
        //    regexp: /.{0,10}(ватсап|вайбер|видеозвонок|скайп|телега).{0,20}/gmi
        //},
        //{
        //    name: "М/ж, ск лет...",
        //    regexp: /.{0,10}(м\/ж|ск.{0,11}лет|м или ж).{0,10}|^[А-Яа-я][?\d\s]{0,3}$|^.{0,3}(парень|девушка|пол|обмен|кто|ж\B|д\B|п\B).{0,1}$/gmi
        //},
        //{
        //    name: "Больше 1200 символов",
        //    regexp: /.{1200}/m
        //}
    ],
    debug: true,        // Отладочные сообщения в консоли
    lastAction: '#newDialog',
    lastPhrase: "",
    messagesCount: 0,
    messageLog: [],
    timerType: 0 // 0 - отключение по таймеру, 1 - отключение по фильтру
}

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

// Вызывает callback(), если элемент el был удалён
function onRemove(el, callback) {
    new MutationObserver((mutations, observer) => {
        if (!document.body.contains(el)) {
            observer.disconnect();
            callback();
        }
    }).observe(document.body, {childList: true, subtree: true});
}

// Вспомогательная функиця для querySelectorNG
function querySelectorNG_Callback(query, callback) {
    let el = document.querySelector(query);

    if (el) {
        console.log("Element found:" + query)
        callback(el);
        //onRemove(el, () => querySelectorNG(query, callback))
    }

    return el;
}

// Выбирает первый элемент с селесктором query и вызывает для него callback
// Если элемент был удалён и создан заного - вызывает callback заного
// Если элемент ещё не создан - дожидается его создания
function querySelectorNG(query, callback) {
    let el = querySelectorNG_Callback(query, callback);
    if (!el)
        new MutationObserver((mutations, observer) => {
            if (querySelectorNG_Callback(query, callback))
                observer.disconnect();
        }).observe(document.body, {childList: true, subtree: true});
    return el;
}

// Срабатывает, каждый раз, когда появляется новый потомок у родительского элемента
function onChildAdd(element, query, callback) {
    new MutationObserver((mutations, observer) => {
        for (const {addedNodes} of mutations) {
            for (const node of addedNodes) {
                if (node.matches(query))
                    callback(node, observer);
            }
        }
    }).observe(element, {childList: true, subtree: true});
}

// Срабатывает, каждый раз, когда меняется видимость элемента
function onVisibilityChanged(el, callback) {
    let observer = new IntersectionObserver((entries, observer) => {
        entries.forEach(entry => {
            callback(el, entry.intersectionRatio > 0, observer);
        });
    }, {root: document.documentElement});

    observer.observe(el);
}

// Альтернатива onVisibilityChanged
function onVisibilityChangedNG(el, callback) {
    new MutationObserver(function (mutations, observer) {
        let visible = el.style.visibility !== "hidden" && el.style.visibility !== "hidden";
        callback(el, visible, observer);
    }).observe(el, {attributes: true});
}

// Срабатывает один раз, когда элемент становится видимым
function onVisible(el, callback) {
    onVisibilityChanged(el, (el, vis, obs) => {
        if (vis) {
            callback(el);
            obs.disconnect();
        }
    })
}

// Bootstrap Dropdown без JQuery
function initBsDropDown() {
    let dropdowns = document.querySelectorAll('[data-toggle=dropdown]');
    for (let dropdown of dropdowns) {
        dropdown.onclick = function (event) {
            let menu_div = dropdown.parentElement;
            if (menu_div.classList.contains("open"))
                return;

            event.stopPropagation();
            menu_div.classList.add("open");

            let menu_list = menu_div.querySelector(".dropdown-menu");
            document.addEventListener('click', function handler(event) {
                if (!menu_list.contains(event.target)) {
                    menu_div.classList.remove("open");
                    this.removeEventListener('click', handler);
                }
            })
        }
    }
}

// Создание полосы загрузки
function createProgressbar(element, duration, callback) {

    element.classList.add('progress');
    element.classList.add('progress-countdown');
    element.innerHTML = "";

    let progressbar_inner = document.createElement('div');
    progressbar_inner.className = 'progress-bar';

    progressbar_inner.style.animationDuration = duration;

    if (typeof (callback) === 'function') {
        progressbar_inner.addEventListener('animationend', callback);
    }

    element.appendChild(progressbar_inner);

    progressbar_inner.style.animationPlayState = 'running';
}

// Возвращает только текст из элемента
function getTextOnly(el) {
    let elClone = el.cloneNode(true);
    let images = elClone.querySelectorAll('img');
    for (let image of images)
        image.outerHTML = image.alt;
    elClone.innerHTML = elClone.innerHTML.replace("<div></div>", "\n");
    return elClone.textContent;
}


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

function createDropdown() {
    let chat_btn = document.querySelector(".main_chat_but")
    chat_btn.insertAdjacentHTML("beforeend", `
<div class="btn-group">
<button type="button" data-toggle="dropdown" class="btn btn-md btn-my1">Начать новый <span class="caret"></span></button>
<ul class="dropdown-menu dropdown-menu-right">
<li><a id="newDialog" href="#">Новый диалог</a></li>
<li><a id="newDialogPhrase" href="#">С той же фразы</a></li>
<li><a id="newDialogSettings" href="#">Открыть настройки</a></li>

<li class="divider"></li>

<li class="checkbox">
<input id="autoDialog" type="checkbox" >
<label class="checkbox">
Автоматически
</label>
</li>

<li class="divider"></li>

<li class="checkbox">
<input id="skipNoAnswer" type="checkbox" >
<label class="checkbox">
Пропускать, если нет ответа <abbr id="skipDelay" title="Изменить значение можно в коде скрипта">1000</abbr> сек.
</label>
</li>

<li class="divider"></li>

<li class="checkbox">
<input id="skipFilter" type="checkbox">
<label class="checkbox">
Пропускать нежелательные сообщения 
<abbr title="Пропускает сообщения, в соответствии с заданными фильтрами
(например, содержащие нецезурную лексику). 
Отредактировать фильтры можно в коде скрипта.">(?)</abbr>
</label>
</li>

<li class="divider"></li>
<li><a id="saveCurrentDialog" href="#">Показать диалог</a></li>
<li><a id="saveAllDialog" href="#">Показать историю</a></li>
</ul>
</div>
`);

    initBsDropDown();

    readSettings();

    document.getElementById("autoDialog").onclick = function (ev) {
        options.autoDialog = ev.target.checked;
        readSettings();

        stopTimer(1);
    }

    document.getElementById("skipNoAnswer").onclick = function (ev) {
        options.skipNoAnswer = ev.target.checked;
        readSettings();

        stopTimer();
    }

    document.getElementById("skipFilter").onclick = function (ev) {
        options.skipFilter = ev.target.checked;
        readSettings();
    }

    document.getElementById("newDialog").onclick = newDialogClick;
    document.getElementById("newDialogPhrase").onclick = newDialogClick;
    document.getElementById("newDialogSettings").onclick = newDialogClick;
    document.getElementById("saveCurrentDialog").onclick = saveCurrentDialogClick;
    document.getElementById("saveAllDialog").onclick = saveCurrentDialogClick;

    let header_div = document.querySelector(".header_chat");
    let progress_div = document.createElement("div");
    progress_div.id = "progressbar_countdown";
    header_div.parentNode.insertBefore(progress_div, header_div.nextSibling);
}

function readSettings() {
    if (!options.autoDialog) {
        options.skipNoAnswer = false
        options.skipFilter = false;
    }

    let autoDialog = document.getElementById("autoDialog");
    autoDialog.checked = options.autoDialog;

    let skipNoAnswer = document.getElementById("skipNoAnswer");
    skipNoAnswer.checked = options.skipNoAnswer;
    skipNoAnswer.disabled = !options.autoDialog;

    let skipFilter = document.getElementById("skipFilter");
    skipFilter.checked = options.skipFilter;
    skipFilter.disabled = !options.autoDialog;

    document.getElementById("skipDelay").innerText = options.skipDelay;
}

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

document.addEventListener("DOMContentLoaded", function(event) {
    checkContainer();
}, { once: true });


function checkContainer() {
    if (document.querySelector('.talk_over')) {
        nektoScript();
    } else {
        setTimeout(checkContainer, 50);
    }
}

// Обработчик нажатий на кнопки перехода к новому диалогу
function newDialogClick(ev) {
    if (ev) {
        ev.preventDefault();
        options.lastAction = ev.target.id;
    }

    // Проверка активности кнопки "Отключиться". Если на неё нельзя нажать, значит диалог уже завершён.
    let disconnect_btn = document.querySelector('.main_chat_but > button.btn');
    if (!disconnect_btn.classList.contains('disabled')) {
        // Нажатие на кнопку "Отключиться"
        disconnect_btn.click();
        // Подтверждение завершения диалога
        querySelectorNG('.swal2-confirm', (el) => el.click());
    } else
        newDialog(true);

    onVisible(document.querySelector('.talk_over_button'), () => newDialog(true));
}

// Обработчик нажатий на кнопки сохранения диалога в виде текста
function saveCurrentDialogClick(ev) {
    let tab = window.open('about:blank', '_blank');
    let content = getCurrentDialog();
    if (ev.target.id === 'saveAllDialog')
        content = [...options.messageLog, ...content];
    content = "<pre style='font-size: 1.2em;white-space: pre-wrap;'>" + content.join('\n') + "</pre>";
    tab.document.write(content);
    tab.document.close();
}

// Возвращает массив, состоящий из строк текущего диалога
function getCurrentDialog() {
    let messages = document.querySelectorAll('.mess_block');
    let content = [];
    for (let message of messages) {
        let txt_message = message.classList.contains('self') ? "Вы" : "Собеседник";
        let txt_time = getTextOnly(message.querySelector('.window_chat_dialog_time'));
        txt_message += " (" + txt_time + ")";
        txt_message += ": ";
        txt_message += getTextOnly(message.querySelector('.window_chat_dialog_text'));
        content.push(txt_message);
    }

    return content;
}


// Выполнеие действий, после отключения собеседника
// force - действие выполняется принудительно по кнопке
function newDialog(force = false) {
    stopTimer(1);

    let over_text = document.querySelector('.talk_over_text').textContent;
    let over_by_nekto = over_text.indexOf('Собеседник') !== -1;

    if (options.autoDialog && over_by_nekto || force) {

        if (options.lastAction === "newDialogPhrase") {
            let first_message = document.querySelector('.self .window_chat_dialog_text');
            if (first_message)
                options.lastPhrase = first_message.innerHTML;
        } else
            options.lastPhrase = "";

        // Запись диалога в историю
        let current_dialog = getCurrentDialog();
        options.messageLog.push(...current_dialog);
        options.messageLog.push("------------------");

        if (options.lastAction === "newDialogSettings")
            document.querySelector(".talk_over_button.blue_bg").click();
        else
            document.querySelector(".talk_over_button:not(.blue_bg)").click();
    }
}

// Запуск таймера. lv = 1 - таймер запускается из-за срабатывания фильтра
function startTimer(lv) {
    options.timerType = parseInt(lv) || 0;
    let progress_div = document.getElementById("progressbar_countdown");
    createProgressbar(progress_div, (lv ? 5 : options.skipDelay) + "s", () => newDialogClick());
}

// Остановка таймера
function stopTimer(lv) {
    lv = parseInt(lv) || 0;
    if (lv >= options.timerType) {
        document.getElementById("progressbar_countdown").innerHTML = "";
        options.timerType = 0;
    }

}

// Выполняется, при появлении нового сообщения в диалоге
function newMessage(el) {
    options.messagesCount++;

    if (options.messagesCount)
        stopTimer();

    // Отключить автоматический переход к новому диалогу, если сообщений больше maxNoSkipCount
    if (options.messagesCount > options.maxNoSkipCount) {
        options.autoDialog = false;
        readSettings();
    }

    // Фильтруем сообщения
    if (options.messagesCount <= options.filterCount) {
        let txt_msg = el.querySelector('.window_chat_dialog_text').innerText;
        for (let filter of options.filter)
            if (filter.regexp.test(txt_msg))
                startTimer(1);
    }
}

function nektoScript() {
    createDropdown();

    // Событие начала нового диалога, каждый раз, когда создаётся поле ввода текста
    // Происходит также и при измененении размеров окна!
    querySelectorNG('.emojionearea-editor', function editorCreate(el) {
        options.messagesCount = getCurrentDialog().length;

        // Обработка нового сообщения в диалоге
        onChildAdd(document.querySelector('.window_chat_block'), '.mess_block:not([style])', newMessage);

        // Отправка фразы, с которой начался предыдущий диалог
        if (options.lastAction === "newDialogPhrase" && options.messagesCount === 0) {

            el.innerHTML = options.lastPhrase;
            if (el.innerText.length) {
                options.messagesCount--; // Исправляем ситуацию, когда таймер не запускается
                document.querySelector('.sendMessageBtn').click();
            }
        }

        // Запуск таймера, если такая опция установлена
        if (options.skipNoAnswer && options.messagesCount < 1) {
            startTimer();
            // Запуск/остановка таймера при появлении сообщения "собеседник набирает сообщение"
            onVisibilityChangedNG(document.querySelector('.window_chat_dialog_write span'),
                (el, vis, obs) => {
                    // Остановить таймер, если есть хотя бы одно сообщение
                    if (options.messagesCount > 0) {
                        obs.disconnect();
                        return;
                    }

                    if (vis)
                        stopTimer();
                    else
                        startTimer();
                });

            // Остановка таймера, если я начинаю печатать
            el.addEventListener('input', () => stopTimer(1));
        }

        // Автоматическое выполнение действия, если собеседник отключился
        onVisible(document.querySelector('.talk_over_button'), () => newDialog());

        // Вызывать эту же функцию, после удаления и создания новго поля ввода
        onRemove(el, () => querySelectorNG('.emojionearea-editor', editorCreate));
    })
}