Emanon

Improves user experience on mangalib.me

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

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