Emanon

Improves user experience on mangalib.me

2019-04-14 या दिनांकाला. सर्वात नवीन आवृत्ती पाहा.

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey, Greasemonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey किंवा Violentmonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

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

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला Tampermonkey यासारखे एक्स्टेंशन इंस्टॉल करावे लागेल..

ही स्क्रिप्ट इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्क्रिप्ट व्यवस्थापक एक्स्टेंशन इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्क्रिप्ट व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला Stylus सारखे एक्स्टेंशन इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

ही स्टाईल इंस्टॉल करण्यासाठी तुम्हाला एक युझर स्टाईल व्यवस्थापक इंस्टॉल करावे लागेल.

(माझ्याकडे आधीच युझर स्टाईल व्यवस्थापक आहे, मला इंस्टॉल करू द्या!)

// ==UserScript==
// @name         Emanon
// @version      2.5.0
// @description  Improves user experience on mangalib.me
// @author       abara
// @match        https://mangalib.me/*
// @match        https://yaoilib.me/*
// @match        https://ranobelib.me/*
// @grant        GM.xmlHttpRequest
// @grant        GM_addStyle
// @connect      coub.com
// @connect      catbox.moe
// @icon         https://mangalib.me/uploads/users/57692/5N9wWaulef.jpg
// @namespace    https://greasyfork.org/users/209098
// @license      MIT
// ==/UserScript==

/**
 * Need refactoring. 
 * Also, feel free to fork.
 */
(function() {
    'use strict';
    const YT_LINK_REGEX = /(?:https?:\/\/)?(?:m\.)?(?:www\.)?(?:youtu\.be\/|youtube\.com\/(?:embed\/|v\/|watch\?v=|watch\?.+&v=))((\w|-){11})(?:\S+)?/;
    const YT_API_KEY = 'AIzaSyA04eUTmTP3skSMcRXWeXlBNI0luJ2146c';
    const IMG_LINK_REGEX = /(?:https?:\/\/)i\.imgur\.com\/(\w+)\.([a-z0-9]{3})/;
    const IMG_CLIENT_ID = '21bf3832d7a7e8f';
    const COUB_LINK_REGEX = /(?:https?:\/\/)coub\.com\/view\/(\w+)/;
    const CATBOX_LINK_REGEX = /https:\/\/files\.catbox\.moe\/(\w+)\.(webm|mp4)/;
    const _store = {};
    // Bad..
    const _default_config = {
        volume: 50,
    };

    class Api {
        constructor() {
            this.nodeMutatons = [];
            this.messageMutations = [];
        }

        // ex. unsafeWindow.emanon.addNodeMutation(node => {//do something});
        addNodeMutation(callback) {
            if (!callback) throw new Error('Invalid node mutaion!');
            this.nodeMutatons.push(callback);
        }

        // ex. unsafeWindow.emanon.addMessageMutation(message => {//do something});
        addMessageMutation(callback) {
            if (!callback) throw new Error('Invalid message mutaion!');
            this.messageMutations.push(callback);
        }
    }

    const emanonApi = new Api();
    // Insec..
    unsafeWindow.emanon = emanonApi;

    class CacheItem {
        /**
         * @param {string} id 
         * @param {string} title 
         * @param {number} createdAt 
         */
        constructor(id, title, createdAt) {
            this.id = id;
            this.title = title;
            this.createdAt = createdAt;
        }
    }

    class Cache {
        /**
         * @param {string} id 
         * @returns {CacheItem}
         */
        static getItem(id) {
            const cache = Storage.getItem('cache');
            if (cache.length) return cache.find(item => item.id == id);
        }

        /**
         * @param {string} id 
         * @param {string} title 
         */
        static setItem(id, title) {
            const cache = Storage.getItem('cache');
            cache.push(new CacheItem(id, title, Date.now()));
            Storage.setItem('cache', cache);
        }

        static truncateOld() {
            const cache = Storage.getItem('cache');
            const truncated = cache.filter(item => item.createdAt >= (Date.now() - 60*1000*30)); // 30min
            Storage.setItem('cache', truncated);
        }
    }

    class User {
        /**
         * @param {string} id 
         * @param {string} note 
         * @param {boolean} blocked 
         */
        constructor(id, note, blocked) {
            this.id = id;
            this.note = note;
            this.blocked = blocked;
        }
    }

    class MessageCounter {
        constructor() {
            this.counter = 0;
        }

        incrementCounter() {
            this.counter += 1;
        }

        resetCounter() {
            this.counter = 0;
        }
    }

    class TitleCounter extends MessageCounter {
        constructor() {
            super();
            this.title = document.title;
        }

        incrementCounter() {
            super.incrementCounter();
            document.title = `(+${this.counter}) ${this.title}`;
        }

        resetCounter() {
            super.resetCounter();
            document.title = this.title;
        }
    }

    class ChatMessageCounter extends MessageCounter {
        constructor() {
            super();
            this.el = makeEl('span');
        }

        incrementCounter() {
            super.incrementCounter();
            this.el.innerText = `(+${this.counter})`;
        }

        resetCounter() {
            super.resetCounter();
            this.el.innerText = '';
        }

        get counterEl() {
            return this.el;
        }
    }

    class Storage {
        static fill() {
            for (let k of ['users', 'cache', 'config']) {
                _store[k] = Storage.getItem(k);
            }
        }

        static getItem(key) {
            if (_store[key]) return _store[key];
            return JSON.parse(localStorage.getItem(`emanon.${key}`)) || [];
        }

        static setItem(key, val) {
            _store[key] = val;
            localStorage.setItem(`emanon.${key}`, JSON.stringify(_store[key] || []));
        }
    }

    // Shortcuts
    const q = (sel, el) => (el || document).querySelector(sel);
    const qAll = (sel, el) => (el || document).querySelectorAll(sel);

    /**
     * @param {string} name 
     * @returns {HTMLElement}
     */
    const makeEl = name => document.createElement(name);

    /**
     * https://stackoverflow.com/a/7557433
     * @param {Element} el 
     * @returns {boolean}
     */
    const isElementInViewport = function(el) {
        var rect = el.getBoundingClientRect();
        return (
            rect.top >= 0 &&
            rect.left >= 0 &&
            rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
            rect.right <= (window.innerWidth || document.documentElement.clientWidth)
        );
    }

    /**
     * @param {string} url 
     */
    const createViewBox = function(url) {
        const view = makeEl('div');
        view.classList.add('_emanon-viewbox');

        let mediaEl = null;
        if (/\.(mp4|webm|gifv)/i.test(url)) {
            mediaEl = makeEl('video');
            mediaEl.autoplay = true;
            mediaEl.loop = true;
            mediaEl.controls = true;

            mediaEl.addEventListener("volumechange", e => {
                const volume = Math.round(100 * e.target.volume);
                const configArr = Storage.getItem('config');
                if (configArr[0]) {
                    configArr[0].volume = volume;
                }
                else {
                    _default_config.volume = volume;
                    configArr.push(_default_config);
                }
                Storage.setItem('config', configArr);
            });

            const configArr = Storage.getItem('config');
            if (configArr[0]) {
                const config = configArr[0];
                mediaEl.volume = 0 == config.volume ? 0 : config.volume / 100;
            }
        }
        else {
            mediaEl = makeEl('img');
        }

        mediaEl.src = url.includes(".gifv") ? url.replace(".gifv", ".mp4") : url;

        view.addEventListener('click', _ => {
            if (mediaEl.tagName == 'VIDEO') {
                mediaEl.pause();
                mediaEl.src = '';
            }
            view.remove();
        });

        view.appendChild(mediaEl);
        q('body').appendChild(view);
    }

    /** 
     * Ported from 4chan-x (MIT). 
     * Thx, Kagami <3
     */
    const getMatroskaTitle = function(body) {
        const data = new Uint8Array(body);
        let i = 0, 
            element = 0, 
            size = 0, 
            title = '';
        
        const readInt = function() {
            let n = data[i++];
            let len = 0;
            while (n < (0x80 >> len)) len++;
            n ^= (0x80 >> len);
            while (len-- && i < data.length) {
                n = (n << 8) ^ data[i++];
            }
            return n;
        }
        
        while (i < data.length) {
            element = readInt();
            size = readInt();
            if (size < 0) break;
            // Title
            if (element === 0x3BA9) {  
                while (size-- && i < data.length) {
                    title += String.fromCharCode(data[i++]);
                }
                return decodeURIComponent(escape(title)); // UTF-8 decoding
            } 
            else if (element !== 0x8538067 &&   // Segment
                element !== 0x549A966) {        // Info
                    i += size;
            }
        }
        return '';
    }

    /**
     * @param {string} userId 
     * @returns {User}
     */
    const getUserById = userId => Storage.getItem('users').find(u => u.id == userId);

    /**
     * @param {string} userId 
     * @returns {boolean}
     */
    const checkIfBlocked = function(userId) {
        const user = getUserById(userId);
        return user && user.blocked;
    }

    /**
     * @param {string} userId 
     * @returns {string}
     */
    const getNoteByUserId = function(userId) {
        const user = getUserById(userId);
        if (user && user.note.length) return user.note;
        return null;
    }

    const getChatWrap = () => _CHAT_INSTANCE.$children[0];

    const getChatInstance = function() {
        const chatWrap = getChatWrap();
        return chatWrap.$children[1] ? 
            chatWrap.$children[1]:
            chatWrap.$children[0];
    }

    /**
     * @param {Element} message 
     * @returns {void}
     */
    const mutateMessage = function(message) {
        // Guard against null
        if (message == null) throw new Error('Message cannot be a null!');

        const userName = getChatInstance().store.auth.username;
        // Get all refs from message
        qAll('span[title]', message).forEach(ref => {
            if (ref.innerText.replace('@', '') == userName) {
                message.classList.add('_emanon-ref');
            }
        });

        qAll('a', message).forEach(link => {
            switch (link.hostname) {
                case 'i.imgur.com':
                if (IMG_LINK_REGEX.test(link.innerHTML)) {
                    if (qAll('a._emanon-image-thumbnail', message).length > 3) break;
                    link.classList.add('_emanon-image-thumbnail');
                    link.innerHTML = link.innerHTML
                        .replace(IMG_LINK_REGEX, (_, name, ext) => `<img src="https://i.imgur.com/${name}t.${ext}" />`);
                }
                break;

                case 'files.catbox.moe':
                if (CATBOX_LINK_REGEX.test(link.innerHTML)) {
                    if (qAll('a._emanon-image-thumbnail', message).length > 3) break;
                    
                    const play = makeEl('i');
                    play.classList.add('fa', 'fa-play-circle', 'play');
                    link.appendChild(play);

                    const overlay = makeEl('div');
                    overlay.classList.add('play-overlay');
                    link.appendChild(overlay);
                    
                    link.classList.add('_emanon-image-thumbnail');
                    link.innerHTML = link.innerHTML
                        .replace(CATBOX_LINK_REGEX, (_, id, ext) => {
                            // Load webm title
                            if (ext == 'webm') {
                                const item = Cache.getItem(id);
                                if (item) link.setAttribute('title', item.title);
                                
                                // Not fancy, but ok
                                GM.xmlHttpRequest({
                                    method: 'GET',
                                    url: link.href,
                                    responseType: 'arraybuffer',
                                    headers: {
                                        Range: 'bytes=0-1023',
                                    },
                                    onload: res => {
                                        if (res.status >= 200 && res.status < 400) {
                                            const title = getMatroskaTitle(res.response);
                                            if (title) {
                                                Cache.setItem(id, title);
                                                link.setAttribute('title', title);
                                            }
                                        }
                                    },
                                    onerror: res => console.error(res.responseText)
                                });
                            }
                            return `<video src="${link.href}" preload="metadata"></video>`;
                        });
                }
                break;

                case 'm.youtube.com':
                case 'youtu.be':
                case 'youtube.com':
                case 'www.youtu.be':
                case 'www.youtube.com':
                link.innerHTML = link.href
                    .replace(YT_LINK_REGEX, (_, id) => {
                        const linkTmp = title => `<i class="fa fa-youtube"></i> Youtube: ${title}`;
                        
                        const item = Cache.getItem(id);
                        if (item) return linkTmp(item.title);
                        
                        fetch(`https://www.googleapis.com/youtube/v3/videos?key=${YT_API_KEY}&part=snippet&id=${id}`)
                        .then(res => res.json())
                        .then(body => {
                            if (body.items.length) {
                                const title = body.items[0].snippet.title;
                                Cache.setItem(id, title);
                                link.innerHTML = linkTmp(title);
                            }
                        })
                        .catch(err => console.error(err));
                        return link.href;
                    });
                break;

                case 'coub.com':
                case 'www.coub.com':
                link.innerHTML = link.href
                    .replace(COUB_LINK_REGEX, (_, id) => {
                        const linkTmp = title => `<i class="fa fa-play-circle"></i> Coub: ${title}`;
                        
                        const item = Cache.getItem(id);
                        if (item) return linkTmp(item.title);

                        GM.xmlHttpRequest({
                            method: 'GET',
                            url: `https://coub.com/api/v2/coubs/${id}`,
                            onload: res => {
                                if (res.status == 200) {
                                    const body = JSON.parse(res.responseText);
                                    Cache.setItem(id, body.title);
                                    link.innerHTML = linkTmp(body.title);
                                }
                            },
                            onerror: res => console.error(res.responseText)
                        });
                        return link.href;
                    });
                break;
            }
        });

        // Apply mutations
        emanonApi.messageMutations.forEach(m => m(message));
    }

    const mutateNode = function(node) {
        console.log("New node added!");
        const avaWrap = q('.chat-item__ava-wrap', node);
        const userId = q('.chat-item__avatar', avaWrap)
            .getAttribute('src').split('/')[3];

        const userNameEl = q('.chat-item__username', node);
        userNameEl.setAttribute('title', userNameEl.innerText);

        // Wrap username
        const userNameWrap = makeEl('div');
        userNameWrap.classList.add("_emanon-username-wrap");
        userNameWrap.appendChild(userNameEl.cloneNode(true));
        userNameEl.remove();
        q('.chat-item__header-content', node).prepend(userNameWrap);
        
        // Add hide icon
        const dateEl = q('.chat-item__date', node);
        const hideEl = makeEl('span');
        hideEl.classList.add('fa', 'fa-eye-slash', 'tooltip', '_emanon-hide-button');
        hideEl.setAttribute('aria-label', 'Скрывать сообщения от этого пользователя');
        hideEl.setAttribute('data-place', 'bottom-end');
        hideEl.addEventListener('click', _ => toogleHidden());
        dateEl.parentNode.insertBefore(hideEl, dateEl);

        if (checkIfBlocked(userId)) {
            node.prepend(makeHiddenBlock());
        }

        function makeHiddenBlock() {
            const aHiddenLink = makeEl('div');
            aHiddenLink.innerText = 'Сообщение скрыто. Раскрыть>>';
            aHiddenLink.classList.add('_emanon-hidden-info');
            aHiddenLink.addEventListener('click', _ => toogleHidden());
            return aHiddenLink;
        }

        function toogleHidden() {
            const hiddenLink = q('._emanon-hidden-info', node);
            const blocked = hiddenLink != null;
            if (blocked) {
                hiddenLink.remove();
            }
            else {
                node.prepend(makeHiddenBlock());
            }

            const users = Storage.getItem('users');
            const index = users.findIndex(u => u.id == userId);
            // Save to storage
            if (index != -1) users[index].blocked = !blocked;
            else users.push(new User(userId, '', !blocked));
            Storage.setItem('users', users);
        }

        // Add note tooltip
        const note = getNoteByUserId(userId);
        if (note) {
            avaWrap.setAttribute('aria-label', note);
            avaWrap.setAttribute('data-place', 'bottom-start');
            avaWrap.classList.add('tooltip', '_emanon-note-tooltip');
        }

        // Apply mutations
        emanonApi.nodeMutatons.forEach(m => m(node));
        
        mutateMessage(q('.chat-item__message', node));
    }

    // Let's play
    GM_addStyle("[contentEditable=true]:empty:not(:focus):before{ content:attr(data-placeholder); color: #999; }");
    GM_addStyle("._emanon-file-input { position: absolute; left: 0; top: 0; width: 40px; height: 40px; opacity: 0; overflow: hidden; z-index: 1; }");
    GM_addStyle("._emanon-icon_container { position: absolute; left: 0; top: 0; width: 40px; height: 40px; text-align: center; font-size: 20px; color: #818181; border: 0; background: none; }");
    GM_addStyle("._emanon-image-thumbnail { display: inline-flex; justify-content: center; align-items: center; vertical-align: top; border: 1px solid #b1b1b1 !important; margin: 1px; line-height: 0; font-size: 0; position: relative; text-decoration: none; }");
    GM_addStyle("._emanon-image-thumbnail:first-child { margin-left: 0; }");
    GM_addStyle("._emanon-image-thumbnail:last-child:not(:only-child) { margin-right: 0; }");
    GM_addStyle("._emanon-image-thumbnail > img, ._emanon-image-thumbnail > video { max-height: 160px; max-width: 160px; }");
    GM_addStyle("._emanon-image-thumbnail .play { position: absolute; font-size: 30px; color: #f77519; z-index: 1; }");
    GM_addStyle("._emanon-image-thumbnail .play-overlay { position: absolute; width: 100%; height: 100%; background: #00000096; z-index: 0; }");
    GM_addStyle("._emanon-viewbox { position: fixed; background: rgba(0,0,0,0.9); z-index: 9999; width: 100%; height: 100%; top: 0; left: 0; display: flex; justify-content: center; align-items: center; }");
    GM_addStyle("._emanon-viewbox > * { width: auto; height: auto; max-width: 99%; max-height: 99%; padding: 0; margin: 0; }");
    GM_addStyle("._emanon-note-tooltip:after { width: 240px; white-space: normal; word-wrap: break-word; padding: 8px; height: initial; z-index: 1; }");
    GM_addStyle("._emanon-hidden-info { text-align: center; color: #cacaca; font-size: 12px; cursor: pointer; }");
    GM_addStyle("._emanon-hidden-info ~ * { display: none; }");
    GM_addStyle("._emanon-hide-button { position: absolute; right: 0; color: #ccc; cursor: pointer; }");
    GM_addStyle("._emanon-hide-button:hover { color: #868e96; }");
    GM_addStyle("._emanon-unread-item { background: #ffecec; }");
    GM_addStyle(".chat-item__header { position: relative; }"); // Fix for hide button
    GM_addStyle(".chat-send > .comment-reply__editor { background: initial !important; min-height: initial; border: none !important; }");
    GM_addStyle("._emanon-username-wrap { width: 90%; display: inline-block; overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }") // Fix long nicknames
    GM_addStyle("._emanon-rotate { transform: rotate(+180deg); transition-duration: 0.5s; }");
    GM_addStyle("._emanon-ref { color: #ff4db6 !important; }");

    // Init storage
    Storage.fill();
    Cache.truncateOld();

    // Append users notes block
    const profileInfo = q('.profile-info');
    if (profileInfo) {
        const userId = window.location.pathname.match(/\d+/)[0];

        const panel = makeEl('div');
        panel.classList.add('aside__panel', 'paper');

        const title = makeEl('h3');
        title.classList.add('aside__title');
        const titleText = makeEl('span');
        titleText.classList.add('aside__title-inner');
        titleText.innerText = 'Заметки о пользователе';
        title.appendChild(titleText);

        const panelBody = makeEl('div');
        panelBody.classList.add('aside__content');
        panelBody.contentEditable = true;
        panelBody.setAttribute('data-placeholder', 'Здесь можно оставить комментарий о пользователе..');

        // Get note from storage
        const note = getNoteByUserId(userId);
        if (note) panelBody.innerText = note;
        
        panelBody.addEventListener('input', e => {
            const target = e.target;
            const users = Storage.getItem('users');
            const index = users.findIndex(u => u.id == userId);
            // Update note
            if (index != -1) {
                users[index].note = target.innerText
            }
            // Add new user
            else {
                users.push(new User(userId, target.innerText, false));
            }
            Storage.setItem('users', users);
        });

        panel.appendChild(title);
        panel.appendChild(panelBody);
        profileInfo.appendChild(panel);
    }
    
    // Init chat
    if (typeof _CHAT_INSTANCE !== 'undefined') {
        // Присасывамся ( ͡° ͜ʖ ͡°)
        const CHAT_WRAP = getChatWrap();
        const CHAT_INSTANCE = getChatInstance();
            
        // Ugly hack
        const chatInitInterval = setInterval(() => {
            console.log('Try to init chat');
            if (CHAT_INSTANCE._isMounted) {
                clearInterval(chatInitInterval);
                console.log('Chat init');
                
                const chat = q('.chat');

                // Add File uploading
                const sendEl = q('.chat-send', chat);
                const fileInput = makeEl('input');
                fileInput.type = 'file';
                fileInput.accept = 'image/*,video/webm,video/mp4';
                fileInput.classList.add('_emanon-file-input');
                
                const iconCont = makeEl('button');
                iconCont.classList.add('_emanon-icon_container');
                const uploadIcon = makeEl('i');
                uploadIcon.classList.add('fa', 'fa-upload');
                
                fileInput.addEventListener('change', e => {
                    const files = e.target.files;
                    if (files.length > 0) {
                        const file = files[0];
                        const sendBtn = q('.chat-send__button', sendEl);

                        // Change icon
                        uploadIcon.classList.remove('fa-upload');
                        uploadIcon.classList.add('fa-spinner', 'fa-spin');
                        sendBtn.disabled = true;
                        fileInput.disabled = true;

                        const resetUpload = function() {
                            uploadIcon.classList.add('fa-upload');
                            uploadIcon.classList.remove('fa-spinner', 'fa-spin');
                            sendBtn.disabled = false;
                            fileInput.value = "";
                            fileInput.disabled = false;
                        }

                        const appendLink = function(link) {
                            let text = CHAT_INSTANCE.store.text;
                            if (text) text = text + '\r\n';
                            CHAT_INSTANCE.store.text = text + link;
                            console.log(`Image link ${link}`);
                        }

                        if (file.type.includes('video')) {
                            const formData = new FormData();
                            formData.append('reqtype', 'fileupload');
                            formData.append('userhash', '');
                            formData.append('fileToUpload', file);

                            GM.xmlHttpRequest({
                                method: "POST",
                                url: 'https://catbox.moe/user/api.php',
                                data: formData,
                                onload: res => {
                                    if (res.status == 200) {
                                        appendLink(res.responseText);
                                    }
                                },
                                onerror: res => console.error(res.responseText),
                                onreadystatechange: xhr => {
                                    if(xhr.readyState === XMLHttpRequest.DONE) {
                                        resetUpload();
                                    }
                                }
                            });
                        }
                        else {
                            const formData = new FormData();
                            formData.append('image', file);
                            const headers = new Headers();
                            headers.append('Authorization', `Client-ID ${IMG_CLIENT_ID}`);
                            
                            fetch('https://api.imgur.com/3/image', { 
                                method: 'POST',
                                body: formData,
                                headers: headers,
                            })
                            .then(res => res.json())
                            .then(body => {
                                if (body.success) {
                                    appendLink(body.data.link);
                                }
                            })
                            .catch(err => console.error(err))
                            .finally(() => resetUpload());
                        }
                    }
                });
                
                iconCont.appendChild(uploadIcon);
                iconCont.appendChild(fileInput);
                // Fix padding
                const textArea = q('.chat-send__area', sendEl);
                textArea.style.paddingLeft = '45px';

                sendEl.prepend(iconCont);

                // Add check messages action
                if (CHAT_WRAP.settings.inWindow) {
                    const checkButton = makeEl('i');
                    checkButton.classList.add('fa', 'fa-fw', 'fa-refresh');
                    checkButton.addEventListener('click', () => {
                        checkButton.classList.remove('_emanon-rotate');
                        setTimeout(() => checkButton.classList.add('_emanon-rotate'), 0);
                        CHAT_INSTANCE.checkMessages();
                    });
                    const actionWrap = makeEl('div');
                    actionWrap.classList.add('chat-wrap__action');
                    actionWrap.appendChild(checkButton);
                    q('.chat-wrap__actions').prepend(actionWrap);
                }
                
                // Working with chat items
                const chatItems = q('.chat__items', chat);
                // Mutate chat items
                qAll('.chat-item', chatItems).forEach(item => mutateNode(item));
                
                chatItems.addEventListener('click', e => {
                    // Add viewbox
                    const target = e.target;
                    if (target.closest('._emanon-image-thumbnail')) {
                            e.preventDefault();
                            createViewBox(target.parentElement.href);
                    }
                    // Remove unread mark
                    qAll('._emanon-unread-item', chatItems)
                        .forEach(item => item.classList.remove('_emanon-unread-item'));
                });
                
                // Add counters
                const titleCounter = new TitleCounter();
                const resetTitle = function() {
                    if (isElementInViewport(chatItems)) {
                        titleCounter.resetCounter();
                    }
                }

                addEventListener('click', _ => resetTitle());
                addEventListener('focus', _ => resetTitle());
                addEventListener('scroll', _ => resetTitle());
                
                const chatCounter = new ChatMessageCounter();
                const chatWrap = q('.chat-wrap__title');
                if (chatWrap) {
                    chatWrap.appendChild(chatCounter.counterEl);
                    // Yep, use timeout
                    chatWrap.addEventListener('click', _ => setTimeout(() => {
                        if (CHAT_WRAP.isOpen) {
                            chatCounter.resetCounter();
                        }
                    }, 100));
                }
                
                let initCounters = true; // HAHAHAH
                // Add chat observer
                const chatObserver = new MutationObserver(mutationsList => {
                    mutationsList.forEach(mutation => 
                        mutation.addedNodes.forEach(node => {
                            if (node.tagName == 'DIV' && 
                                node.classList.contains('chat-item')) {
                                // Increment counters
                                if (!initCounters) {
                                    if (document.hidden) titleCounter.incrementCounter();
                                    if (!CHAT_WRAP.isOpen) chatCounter.incrementCounter();
                                    // Mark as unread
                                    if (document.hidden || chatWrap && !CHAT_WRAP.isOpen) {
                                        node.classList.add('_emanon-unread-item');
                                    }
                                }
                                mutateNode(node);
                            }
                        })
                    )
                    initCounters = false;
                });
                chatObserver.observe(chatItems, {childList: true, subtree: false, attributes: false});
                
                // Add message preview observer
                const previewObserver = new MutationObserver(mutationsList =>
                    mutationsList.forEach(mutation => 
                        mutation.addedNodes.forEach(node => {
                            if (node.classList.contains('tippy-popper') && 
                                // Not already mutated
                                !node.classList.contains('_emanon-mutated')) {
                                    const chatPvInterval = setInterval(() => {
                                        if (node.getAttribute('data-init') == 'true') {
                                            clearInterval(chatPvInterval);
                                            node.classList.add('_emanon-mutated');
                                            mutateMessage(q('.message-preview__content', node));
                                        }
                                    }, 50);
                            }
                        }
                    )
                ));
                previewObserver.observe(q('body'), {childList: true, subtree: false, attributes: false});
            }
        }, 50);
    }
})();