Camamba Chat Tweaks

tweaks layout of the chat

// ==UserScript==
// @name            Camamba Chat Tweaks
// @namespace       dannysaurus.camamba
// @version         0.5.14
// @description     tweaks layout of the chat
// @license         MIT License
//
// @include         https://www.camamba.com/chat/
// @include         https://www.de.camamba.com/chat/
//
// @connect         camamba.com
// @grant           GM_xmlhttpRequest
//
// @require         https://greasyfork.org/scripts/405143-simplecache/code/SimpleCache.js
// @require         https://greasyfork.org/scripts/405144-httprequest/code/HttpRequest.js?version=1106047
// @require         https://greasyfork.org/scripts/391854-enum/code/Enum.js
// @require         https://greasyfork.org/scripts/405699-camamba-user/code/Camamba%20User.js
//
// @require         https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js
//
// @require         https://greasyfork.org/scripts/423722-camamba-chat-helpers-library/code/Camamba%20Chat%20Helpers%20Library.js?version=960246
// @require         https://greasyfork.org/scripts/423662-camamba-chat-settings/code/Camamba%20Chat%20Settings.js?version=913122
// @require         https://greasyfork.org/scripts/423665-camamba-hook-into-onmessage/code/Camamba%20Hook%20Into%20OnMessage.js?version=1180072
//
// @grant           GM.getValue
// @grant           GM.setValue
// ==/UserScript==

// https://greasyfork.org/de/scripts/419077-camamba-chat-tweaks

/* jslint esnext: true */
/* globals knownUsers, me */
(function () {
    'use strict';
    // --- initial sizes ---
    const SIZES = {
        FONT_EM: {
            userList: 1.2,
            chatBox: 1.8,
        },
        WIDTH_EM: {
            sidebarLeft: 10,
            sidebarRight: 14,
        },
    };

    // --- HTML Selector Helpers ---
    const SELECTORS = {
        ID: {
            // original
            userList: 'userList',
            chatBox: 'chatBox',
            chatInput: 'chatInput',
            chatWindow: 'chatWindow',
            mediaContainer1: 'mediaContainer1',
            mediaContainer2: 'mediaContainer2',
            mediaContainer3: 'mediaContainer3',
            mediaContainer4: 'mediaContainer4',
            mediaContainer5: 'mediaContainer5',
            mediaContainer6: 'mediaContainer6',
            mediaContainer7: 'mediaContainer7',
            mediaContainer8: 'mediaContainer8',

            // script
            cbCamslots: 'cb-camslots',
            spinnerUserlistFont: 'spinner-userlist-font',
            spinnerChatFont: 'spinner-chat-font',
            unamePermaInput: 'uname-perma-input',
            cbPrivateConvo: 'cb-privateConvo',
        },
        CLASS: {
            noTextSelect: 'noTextSelect',
            borderBox: 'borderBox',
            camBox: 'camBox'
        }
    };

    const containers = (() => {
        let userList, chatBox, sidebars, camslots;

        return {
            get userList() {
                if (typeof userList === "undefined") {
                    userList = document.getElementById(SELECTORS.ID.userList);
                }
                return userList;
            },

            get chatBox() {
                if (typeof chatBox === "undefined") {
                    chatBox = document.getElementById(SELECTORS.ID.chatBox);
                }
                return chatBox;
            },

            get sidebars() {
                if (typeof sidebars === "undefined") {
                    sidebars = document.getElementById(SELECTORS.ID.chatWindow).querySelectorAll(`.${SELECTORS.CLASS.noTextSelect}`);
                }
                return sidebars;
            },

            get sidebarLeft() {
                return this.sidebars[0];
            },

            get sidebarTop() {
                return this.sidebars[1];
            },

            get sidebarRight() {
                return this.sidebars[2];
            },

            get camslots() {
                if (typeof camslots === "undefined") {
                    const parentContainers = [
                        SELECTORS.ID.mediaContainer1,
                        SELECTORS.ID.mediaContainer2,
                        SELECTORS.ID.mediaContainer3,
                        SELECTORS.ID.mediaContainer4,
                        SELECTORS.ID.mediaContainer5,
                        SELECTORS.ID.mediaContainer6,
                        SELECTORS.ID.mediaContainer7,
                        SELECTORS.ID.mediaContainer8,
                    ]
                        .map(id => document.getElementById(id))
                        .filter(el => el !== null)
                        .map(el => el.parentNode);

                    camslots = [...new Set(parentContainers)];
                }
                return camslots;
            }
        };
    })();

    const layoutPatcher = new class {
        constructor() {
            this.historyCamslotsRemoved = [];
        }

        patchSizes() {
            // this.setWidthOfSidebarLeft(`${SIZES.WIDTH_EM.sidebarLeft}em`);
            this.setWidthOfSidebarRight(`${SIZES.WIDTH_EM.sidebarRight}em`);
            return this;
        }

        setFontSizeOfUserList(fontSize) {
            containers.userList.style.fontSize = fontSize;
            return this;
        }

        setFontSizeOfChat(fontSize) {
            containers.chatBox.style.fontSize = fontSize;
            return this;
        }

        setWidthOfSidebarLeft(width) {
            containers.sidebarLeft.style.width = width;
            return this;
        }

        setWidthOfSidebarRight(width) {
            containers.sidebarLeft.style.width = width;
            return this;
        }

        showCamslots() {
            for (let i = 0; i < this.historyCamslotsRemoved.length; i++) {
                const { parent, index, element } = this.historyCamslotsRemoved.pop();
                parent.insertBefore(element, parent.children[index]);
            }
            return this;
        }

        hideCamslots() {
            for (let element of containers.camslots) {
                const parent = element.parentNode;
                if (parent) {
                    let index = Array.from(parent.children).indexOf(element);
                    parent.removeChild(element);

                    this.historyCamslotsRemoved.push({ parent, index, element });
                }
            }
            return this;
        }
    }();


    const controls = (() => {
        // --- HTML Create Element Helpers ---
        const createInput = ({
            id,
            parentElement = null,
            type = 'text',
            defaultValue = '',
            labelText = null,
            onValueChange = null,
            propertyNameValue = 'value',
            eventNameValueChange = 'input',
        }) => {
            const div = document.createElement('div');

            const input = div.appendChild(document.createElement('input'));
            input.type = type;
            input.id = id;
            input.style.backgroundColor = 'rgba(39,62,77,1)';

            if (labelText) {
                const label = div.appendChild(document.createElement('label'));
                label.htmlFor = id;
                label.appendChild(document.createTextNode(labelText));
            }

            if (onValueChange) {
                let oldValue;

                input.addEventListener(eventNameValueChange, () => {
                    const newValue = input[propertyNameValue];
                    if (oldValue !== newValue) {
                        oldValue = newValue;

                        onValueChange(newValue);
                    }
                });
            }

            if (parentElement) {
                parentElement.appendChild(div);
            }
            return input;
        };

        const createInputPersistent = ({
            id,
            parentElement = null,
            type = 'text',
            defaultValue = '',
            labelText = null,
            onValueChange = null,
            propertyNameValue = 'value',
            eventNameValueChange = 'input',
        }) => {
            const input = createInput({
                parentElement, type, id, defaultValue, labelText, propertyNameValue, eventNameValueChange,
                onValueChange: value => {
                    GM.setValue(id, value);
                    if (onValueChange) {
                        onValueChange(value);
                    }
                }
            });

            input.setValue = value => {
                GM.setValue(id, value);
                input[propertyNameValue] = value;
                onValueChange(value);
            };

            input.updateValue = () => GM.getValue(id, defaultValue).then(value => {
                input[propertyNameValue] = value;
                if (onValueChange) {
                    onValueChange(value);
                }
            });

            return input;
        };

        const createCheckbox = ({
            id,
            parentElement = null,
            initialChecked = false,
            labelText = null,
            onValueChange = null,
        }) => {
            const checkbox = createInputPersistent({
                parentElement, id, labelText, onValueChange,
                defaultValue: !!initialChecked,
                type: 'checkbox',
                propertyNameValue: 'checked',
                eventNameValueChange: 'click',
            });
            return checkbox;
        };

        const createSpinner = ({
            id, min, max, step,
            parentElement = null,
            defaultValue = 0,
            labelText = null,
            onValueChange = null,
        }) => {
            const spinner = createInputPersistent({
                parentElement, id, defaultValue, labelText, onValueChange,
                type: 'number',
            });
            spinner.min = min;
            spinner.max = max;
            spinner.step = step;

            const buttonDec = spinner.parentNode.insertBefore(document.createElement('button'), spinner);
            buttonDec.type = 'button';
            buttonDec.innerHTML = '-';
            buttonDec.addEventListener('click', () => {
                spinner.stepDown();
                spinner.setValue(spinner.value);
            });

            const buttonInc = spinner.parentNode.insertBefore(document.createElement('button'), spinner.nextSibling);
            buttonInc.type = 'button';
            buttonInc.innerHTML = '+';
            buttonInc.addEventListener('click', () => {
                spinner.stepUp();
                spinner.setValue(spinner.value);
            });

            return spinner;
        };

        const sidebarLeftCenter = containers.sidebarLeft.children[1];
        sidebarLeftCenter.innerHTML = "";
        const container = sidebarLeftCenter.appendChild(document.createElement('div'));

        // checkbox camslots on/off
        const cbCamslots = createCheckbox({
            parentElement: container,
            id: SELECTORS.ID.cbCamslots,
            initialChecked: true,
            labelText: 'camslots',
            onValueChange: value => {
                if (value) {
                    layoutPatcher.showCamslots();
                } else {
                    layoutPatcher.hideCamslots();
                }
            },
        });

        // spinner userlist font
        const spinnerUserlistFont = createSpinner({
            parentElement: container,
            id: SELECTORS.ID.spinnerUserlistFont,
            defaultValue: SIZES.FONT_EM.userList,
            min: 1.0,
            max: 3.2,
            step: 0.1,
            labelText: 'users',
            onValueChange: value => {
                const fontSize = `${value}em`;
                layoutPatcher.setFontSizeOfUserList(fontSize);
            },
        });

        // spinner chat font
        const spinnerChatFont = createSpinner({
            parentElement: container,
            id: SELECTORS.ID.spinnerChatFont,
            defaultValue: SIZES.FONT_EM.chatBox,
            min: 1.0,
            max: 5.5,
            step: 0.1,
            labelText: 'chat',
            onValueChange: value => {
                const fontSize = `${value}em`;
                layoutPatcher.setFontSizeOfChat(fontSize);
            },
        });

        const buttonKickFromCam = container.appendChild(document.createElement('button'));
        buttonKickFromCam.type = 'button';
        buttonKickFromCam.innerHTML = 'Kick from cam';
        buttonKickFromCam.addEventListener('click', () => {
            knownUsers.bySelected().stopViewing();
        });

        if (me.admin) {
            const labelUnamePerma = container.appendChild(document.createElement('label'));
            labelUnamePerma.type = 'text';
            labelUnamePerma.for = "uname-perma";
            labelUnamePerma.innerHTML = 'Username Perma';

            const inputUnamePerma = container.appendChild(document.createElement('input'));
            inputUnamePerma.type = 'text';
            inputUnamePerma.id = SELECTORS.ID.unamePermaInput;
            inputUnamePerma.name = 'uname-perma';

            const buttonPerma = container.appendChild(document.createElement('button'));
            buttonPerma.type = 'button';
            buttonPerma.innerHTML = 'perma';
            buttonPerma.addEventListener('click', () => {
                const unamePerma = document.getElementById(SELECTORS.ID.unamePermaInput).value;
                if (unamePerma) {
                    knownUsers.addExact(unamePerma).then(() => knownUsers.byName(unamePerma).banPermaFast(""));
                } else {
                    knownUsers.bySelected().ban("You are permanently banned from Camamba. Please do not create any additional accounts!", 24, { isPublic: true, isPerma: true, suppressBanLog: false });
                }
            });
        }

        const isGerman = location.hostname === "www.de.camamba.com";


        let oldPrivateHandler = null;

        // checkbox camslots on/off
        const cbPrivateConvo = createCheckbox({
            parentElement: container,
            id: SELECTORS.ID.cbPrivateConvo,
            initialChecked: true,
            labelText: isGerman ? 'PN ablehnen ohne Freundschaft' : 'PM denie withouth friendship',
            onValueChange: (value) => {
                if (value) {
                    if (!oldPrivateHandler && onMessageHandlers.private) {
                        oldPrivateHandler = onMessageHandlers.private;
                        console.log("Alter Handler gesichert.", oldPrivateHandler.toString());
                    }
                    /** 
                     * @param {{ id: number }} data  
                     * @return {boolean} - true if further handling is required, false if action is fully handled
                     */
                    onMessageHandlers.private = (data) => {
                        if (!data.id) {
                            return true;
                        }
                        const user = knownUsers[data.id];
                        if (!user) {
                            console.log(`Unknown user with id ${data.id} requesting ${"privConvo"}.`);
                            return false;
                        }

                        if (!user.friend) {
                            wsSend({ command: "control", target: data.id, request: "privReject" });
                            console.log(`PN von ${user.name} abgelehnt`)
                            return false;
                        }
                        console.log(`PN von ${user.name} erlaubt`)
                        console.log("Alter Handler ausgeführt.")
                        if (typeof oldPrivateHandler === 'function') {
                            return oldPrivateHandler(data);
                        }
                        return true;
                    };
                } else {
                    if (oldPrivateHandler) {
                        onMessageHandlers.private = oldPrivateHandler;
                        console.log("Alter Handler wiederhergestellt.")
                    }
                }
            },
        });

        return {
            cbCamslots,
            spinnerUserlistFont,
            spinnerChatFont,
            cbPrivateConvo,
        };
    })();

    const wait = async (ms) => new Promise(res => setTimeout(res, ms));
    (async () => {
        // wait until websocket has been connected
        while (typeof initSettings !== 'function') {
            await wait(100);
        }

        const original = initSettings;
        initSettings = () => {
            original();

            // Breite von Userliste anpassen
            layoutPatcher.patchSizes();

            // weiterere Einstellungen überschreiben, bzw übernehmen
            for (let control of [controls.cbCamslots, controls.spinnerUserlistFont, controls.spinnerChatFont, controls.cbPrivateConvo]) {
                control.updateValue();
            }
        };
    })();

    (async () => {
        let lastBanData = { userId: 0, text: '', time: 0, isPerma: false };

        while (typeof adminExec !== 'function') {
            await wait(100);
        }

        adminExec();

        if (currentAdminAction == "ban") {
            let userId, text, time, isPerma;

            text = byId('adminMessageInput').value;
            if (!text || text.length <= 3 && byId('adminMessageSelect').selectedIndex) {
                text = adminMessages[currentAdminAction][byId('adminMessageSelect').value];
            }

            userId = currentAdminTarget;
            time = parseInt(byId('banTime').value);
            isPerma = byId('permaBan') && byId('permaBan').checked;

            if (userId && text > 3 && time) {
                lastBanData = { userId, text, time, isPerma };
            }
        }
    })();


    (async () => {
        while (document.getElementById(SELECTORS.ID.chatInput) === null) {
            await wait(100);
        }
        document.getElementById(SELECTORS.ID.chatInput).setAttribute('autoComplete', 'on');
    })();

    console.log("running camamba chat tweaks")
})();