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

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

当前为 2022-05-10 提交的版本,查看 最新版本

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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