Krendel / BMPP

// ==UserScript==
// @name         BMPP
// @namespace    http://tampermonkey.net/
// @version      1.0
// @description  Better multiplayer piano experience
// @author       Krendel
// @match        http*://www.multiplayerpiano.com/*
// @grant        GM_log
// @grant        GM_addStyle
// @grant        GM_setValue
// @grant        GM_getValue
// @require      http://code.jquery.com/jquery-3.5.1.min.js
// @run-at       document-start
// @license      MIT
// ==/UserScript==
/* globals jQuery, $, MPP */

unsafeWindow.BMPP = (function() {
    'use strict';

    /* ===== Styles ===== */
    GM_addStyle(`
@import url('https://fonts.googleapis.com/css2?family=Open+Sans&display=swap');

* { image-rendering: auto !important; }
body { -webkit-transition: none !important; }
html, body {
    font-size: 18px !important;
    font-family: 'Open Sans', sans-serif !important;
}

#ban-button { margin-left: 0.3rem; }

#room-controls {
    position: absolute;
    top: 0;
    right: 0;
    margin: 0.2rem 0.2rem;
    text-shadow: 0 0 2px black, 0 0 2px black, 0 0 2px black, 0 0 2px black;
}
#room {
    font-size: 0.8rem !important;
    padding: 0 0.1rem !important;
    height: 1.2rem !important;
    display: flex !important;
    align-items: center !important;
    background-color: #768fa3 !important;
    border-color: #b4d0ff;
    width: 300px !important;
}
#room .more .info {
    background-color: #5c6878 !important;
    overflow: hidden !important;
}
#room .more .info:nth-child(even) {
    background-color: #495668 !important;
}
#room .more {
    background-color: #495668 !important;
    position: unset !important;
    border-color: #b4d0ff;
}
#room .more .new { display: none !important; }
#room .more .info:hover { border-left: solid 2px white; }
#room .expand { background-color: #5c6878 !important; }
#room .info {
    display: flex !important;
    align-items: center !important;
    padding: 0.2rem 0 !important;
    line-height: unset !important;
    text-shadow: 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black;
}
#room .info.lobby { color: unset !important; }
#room .info.lobby .room-name { color: #7fc1ff; }
#room .info.not-visible { color: #353f49 !important; }

#bottom {
    background-color: #040c1c !important;
    height: 64px !important;
}

.ugly-button {
    font-size: 0.8rem !important;
    background-color: rgba(172,219,255,0.3) !important;
    -webkit-border-radius: 5px !important;
    border: solid 1px black !important;
    height: 14px !important;
    display: flex !important;
    align-items: center;
}
.ugly-button:hover { border-color: white !important; }

#names .name.me:after { content: "" !important; }
#names .name.me { border: outset 4px white !important; }
#names .name {
    text-shadow: 0 0 2px black, 0 0 2px black, 0 0 2px black, 0 0 2px black;
    font-weight: bold !important;
    font-size: 0.8rem !important;
}

::-webkit-scrollbar { width: 10px !important; }
::-webkit-scrollbar-track {
    background: #495668 !important;
    border-radius: 10px !important;
}
::-webkit-scrollbar-thumb {
    background: #768fa3 !important;
    border-radius: 10px !important;
}

#chat {
    display: flex !important;
    flex-direction: column !important;
    align-items: stretch !important;
    justify-content: stretch !important;
    bottom: 68px !important;
}
#chat input {
    width: unset !important;
    height: 1rem !important;
    font-family: inherit !important;
    font-size: 0.8rem !important;
}
#chat input:focus { border: solid 1px #5eb2ff !important; }
#chat.chatting {
    background-color: rgba(121,124,181,0.3) !important;
    box-shadow: none !important;
}
#chat.chatting ul { overflow-y: auto !important; }
#chat ul { overflow-wrap: break-word !important; }

#quota { background-color: #c4f8ff !important; }
#quota .value { background-color: #325d97 !important; }

.room-name {
    background-color: rgba(14,12,28,0);
    pointer-events: none;
    display: flex;
    justify-content: center;
}
.room-name-wrapper {
    z-index: 10;
    pointer-events: none;
}
#room .more .info:hover > .room-name-wrapper { display: block; }

.rooms-wrapper {
    position: absolute;
    width: 100%;
    bottom: 100%;
    left: -1px;
}

.count {
    display: flex;
    align-items: center;
    z-index: 100;
    justify-content: center;
    padding: 0.2rem 0.2rem;
    margin-right: 0.3rem;
    min-width: 1rem;
    background-color: #768fa3;
}

#Notification-share { display: none !important; }
#social { display: none !important; }
.dialog {
    background-color: rgba(62,78,110,0.8) !important;
    border-color: #b4d0ff !important;
    text-shadow: 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black !important;
}
.dialog .submit {
    text-shadow: 0 0 1px black, 0 0 1px black, 0 0 1px black, 0 0 1px black !important;
    padding: 0.5rem 1.5rem !important;
    right: 0 !important; bottom: 0 !important;
    display: flex !important;
    align-items: center;
    justify-content: center;
    outline: none !important;
}
.submit {
    background-color: #80b2e0 !important;
    -webkit-box-shadow: none !important;
    box-shadow: none !important;
    border-radius: 4px 0 0 0 !important;
}
.submit:hover {
    border-left: solid #b4d0ff 1px !important;
    border-top: solid #b4d0ff 1px !important;
}
.drop-crown { margin-left: 14px !important; }
.notification-body {
    background-color: #040c1c !important;
    color: white !important;
    border: solid 1px #b4d0ff !important;
    text-shadow: none !important;
}
.notification .x {
    color: #b4d0ff !important;
    font-size: 1.5rem !important;
}
.title { border-color: #b4d0ff !important; }
.notification .pack, .notification .connection { border-color: #b4d0ff !important; }
.notification .pack.enabled, .notification .connection.enabled { background-color: rgba(138,183,255,0.4) !important; }
.notification .pack.enabled::after { color: #d7e2ff !important; }
.notification .connection { background-color: #040c1c !important; }

.button-container {
    display: flex;
    flex-wrap: wrap;
    margin: 2px;
    margin-left: 350px;
    min-width: 466px;
    max-width: 900px;
}
#new-room-btn, #play-alone-btn, #room-settings-btn, #midi-btn, #record-btn, #synth-btn, #sound-btn {
    position: unset !important;
    margin: 2px;
}

.marquee > .marquee-text {
    animation: marquee 10s linear infinite;
}
@keyframes marquee {
    0% {
        -moz-transform: translateX(0%);
        -webkit-transform: translateX(0%);
        transform: translateX(0%);
    }
    100% {
        -moz-transform: translateX(-100%);
        -webkit-transform: translateX(-100%);
        transform: translateX(-100%);
    }
}

.relative {
    display: flex;
    justify-content: space-between;
}
.volume-container {
    display: flex;
    flex-direction: column;
    margin: 8px 5px;
}
#volume {
    position: unset !important;
    margin: 0 !important;
    width: 140px !important;
    height: 20px !important;
}
#volume-slider { background-size: contain !important; }
#volume-label {
    position: unset !important;
    text-align: right;
}

input[type=range]#volume-slider {
    -webkit-appearance: none; /* Hides the slider so that custom slider can be made */
    width: 100%; /* Specific width is required for Firefox. */
}

input[type=range]#volume-slider:focus {
    outline: none; /* Removes the blue border. */
}

input[type=range]#volume-slider::-ms-track {
    width: 100%;
    cursor: pointer;

    /* Hides the slider so custom styles can be added */
    background: transparent;
    border-color: transparent;
    color: transparent;
}

/* Special styling for WebKit/Blink */
input[type=range]#volume-slider::-webkit-slider-thumb {
    -webkit-appearance: none;
    border: 1px solid #000000;
    height: 20px;
    width: 10px;
    border-radius: 3px;
    background: #ffffff;
    cursor: pointer;
    margin-top: -14px;
    box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
    position: relative;
    top: 6px;
}

/* All the same stuff for Firefox */
input[type=range]#volume-slider::-moz-range-thumb {
    box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
    border: 1px solid #000000;
    height: 20px;
    width: 10px;
    border-radius: 3px;
    background: #ffffff;
    cursor: pointer;
    position: relative;
    top: 6px;
}

/* All the same stuff for IE */
input[type=range]#volume-slider::-ms-thumb {
    box-shadow: 1px 1px 1px #000000, 0px 0px 1px #0d0d0d;
    border: 1px solid #000000;
    height: 20px;
    width: 10px;
    border-radius: 3px;
    background: #ffffff;
    cursor: pointer;
    position: relative;
    top: 6px;
}

.autoban-container {
    position: absolute;
    top: 40px;
    right: 0;
    margin: 0.2rem 0.2rem;
    text-shadow: 0 0 2px black, 0 0 2px black, 0 0 2px black, 0 0 2px black;
    text-align: right;
}

#autoban-timeout { width: 30px; }


.lbl-toggle:focus {
    outline: none;
}

input[type='checkbox'].toggle {
    display: none;
}

.lbl-toggle {
    display: block;

    font-weight: bold;
    text-transform: uppercase;
    text-align: center;

    color: #5d81c8;
    background: rgba(20, 20, 38, 0.5);

    cursor: pointer;

    border-radius: 5px;
    transition: all 0.25s ease-out;
}

.lbl-toggle:hover {
    color: #395999;
}

.lbl-toggle::before {
    content: '';
    display: inline-block;

    border-top: 5px solid transparent;
    border-bottom: 5px solid transparent;
    border-left: 5px solid currentColor;
    vertical-align: middle;
    margin-right: .7rem;
    transform: translateY(-2px);

    transition: transform .2s ease-out;
}

.toggle:checked + .lbl-toggle::before {
    transform: rotate(90deg) translateX(-3px);
}

.collapsible-content {
    max-height: 0px;
    overflow: hidden;
    transition: max-height .25s ease-in-out;
}

.toggle:checked + .lbl-toggle + .collapsible-content {
    max-height: 100vh;
}

.toggle:checked + .lbl-toggle {
    border-bottom-right-radius: 0;
    border-bottom-left-radius: 0;
}

.collapsible-content .content-inner {
    background: rgba(74, 93, 150, .2);
    border-bottom: 1px solid rgba(120, 190, 255, .5);
    border-bottom-left-radius: 5px;
    border-bottom-right-radius: 5px;
    padding: .5rem;
}

.collapsible-content label {
    float: left;
}
.collapsible-content input:focus {
    outline: none;
}
.collapsible-content input[type="text"] {
    margin-left: 10px;
}



.sort-container {
    height: auto !important;
    padding: 0 !important;
    position: sticky;
    top:0;
    z-index:1000;

    cursor: auto !important;

    display: flex;
    align-items: center;
    justify-content: space-between;

    background-color: #2e3547;
}

.sort-toggle {
    font-size: 0.7rem;
    font-weight: bold;
    text-transform: uppercase;
    text-align: center;
    padding: .2rem .4rem !important;
    margin-right: .4rem !important;
    color: #5d81c8;
    flex-grow: 1;
    position: relative;

    cursor: pointer;

    transition: all 0.25s ease-out;
}

.sort-toggle.sort-asc::after, .sort-toggle.sort-desc::after {
    content: '';
    display: inline-block;

    border-left: 5px solid transparent;
    border-right: 5px solid transparent;

    vertical-align: middle;
    transform: translateY(-2px);

    position: absolute;
    right: 0;
    top: 12px;

    transition: transform .2s ease-out;
}

.sort-toggle.sort-asc::after {
    border-bottom: 5px solid currentColor;
}

.sort-toggle.sort-desc::after {
    border-top: 5px solid currentColor;
}
`);

    /* ==== Predefined elements ==== */
    const roomControlsDiv = `
<div id="room-controls">
<label for="room-color">Room color:</label>
<input type="color" id="room-color">
</div>`;
    const banButton = '<button id="ban-button">Ban</button>';
    const roomsWrapper = '<div class="rooms-wrapper"></div>';
    const roomInfoTemplate = '<div class="info"><div class="count"></div><div class="room-name-wrapper"><div class="room-name"></div></div></div>';
    const buttonContainer = '<div class="button-container"></div>';
    const volumeContainer = '<div class="volume-container"></div>';
    const autobanContainer = `
<div class="autoban-container">
    <input id="collapsible" class="toggle" type="checkbox">
    <label for="collapsible" class="lbl-toggle" tabindex="0">Autoban Options</label>
    <div class="collapsible-content">
        <div class="content-inner">
            <div>
                <label for="autoban-timeout">Timeout (m): </label>
                <input type="number" id="autoban-timeout">
            </div>
            <div>
                <label for="autoban-anonymous">Anonymous: </label>
                <input type="checkbox" id="autoban-anonymous">
            </div>
            <div>
                <label for="autoban-russian-nicknames">Russian nicknames: </label>
                <input type="checkbox" id="autoban-russian-nicknames">
            </div>
            <div>
                <label for="autoban-regex">Regex: </label>
                <input type="text" id="autoban-regex">
            </div>
        </div>
    </div>
</div>
`;
    const sortContainerTemplate = `
<div class="sort-container">
    <div class="sort-by-creation sort-toggle">by creation</div>
    <div class="sort-by-name sort-toggle">by name</div>
    <div class="sort-by-count sort-toggle">by count</div>
</div>
`;

    const sortType = {
        byCreationAsc: 0,
        byCreationDesc: 1,
        byNameAsc: 2,
        byNameDesc: 3,
        byCountAsc: 4,
        byCountDesc: 5
    }

    const notificationMidiSelector = "#Notification-MIDI-Connections";

    const rooms = [];

    // remove all translations
    $(".translate").removeClass("translate");

    $(function() {
        const $ = unsafeWindow.$;

        $.fn.insertAt = function(index, element) {
            var lastIndex = this.children().length;
            if (index < 0) {
                index = Math.max(0, lastIndex + 1 + index);
            }
            this.append(element);
            if (index < lastIndex) {
                this.children().eq(index).before(this.children().last());
            }
            return this;
        }

        /* ==== functions ==== */
        function elementReady(selector) {
            return new Promise((resolve, reject) => {
                const el = $(selector)[0];
                if (el) {resolve(el);}
                new MutationObserver((mutationRecords, observer) => {
                    // Query for elements matching the specified selector
                    Array.from($(selector).toArray()).forEach((element) => {
                        resolve(element);
                        // Once we have resolved we don't need the observer anymore.
                        observer.disconnect();
                    });
                }).observe(document.documentElement, {
                    childList: true,
                    subtree: true
                });
            });
        }

        function isParticipantOwner(p) {
            let channel = MPP.client.channel;
            return channel && channel.crown && channel.crown.participantId === p.id;
        }

        function ban(p) {
            if (isParticipantOwner(p)) return;
            let timeoutSec = parseInt(GM_getValue("autobanTimeout"));
            MPP.client.sendArray([{m: "kickban", _id: p._id, ms: (!isNaN(timeoutSec) && timeoutSec >= 1 && timeoutSec <= 60 ? timeoutSec : 1) * 60 * 1000 }]);
            console.log(`Tried to ban ${p}`);
        }

        function banIfAnon(p) {
            if (p.name.toLowerCase() === "anonymous") {
                ban(p);
            }
        }

        function banIfRussianNickname(p) {
            if (/[а-яА-ЯЁё]/.test(p.name)) {
                ban(p);
            }
        }

        function banIfRegexMatched(p, regex) {
            if (RegExp(regex).test(p.name)) {
                ban(p);
            }
        }

        function autobanUser(p) {
            if (!MPP.client.isOwner() || MPP.client.channel.settings.lobby || isParticipantOwner(p)) return;

            if (GM_getValue("autobanAnonymous")) {
                banIfAnon(p);
            };
            if (GM_getValue("autobanRussianNicknames")) {
                banIfRussianNickname(p);
            }
            let regex = GM_getValue("autobanRegex");
            if (regex) {
                banIfRegexMatched(p, regex);
            }
        }

        function invertHex(hex) {
            let isPrefixed = false;

            if(hex.length < 6 && hex.length > 7) {
                console.error("Hex color must be six hex numbers in length.");
                return false;
            }
            if(hex.length == 7 && hex[0] != '#') {
                console.error("Hex color must begin with #.");
                return false;
            }

            isPrefixed = hex.length == 7;
            if (isPrefixed) {
                hex = hex.substring(1);
            }

            hex = hex.toUpperCase();
            let splitNum = hex.split("");
            let resultNum = "";
            let simpleNum = "FEDCBA9876".split("");
            let complexNum = new Array();
            complexNum.A = "5";
            complexNum.B = "4";
            complexNum.C = "3";
            complexNum.D = "2";
            complexNum.E = "1";
            complexNum.F = "0";

            for(let i=0; i<6; i++){
                if(!isNaN(splitNum[i])) {
                    resultNum += simpleNum[splitNum[i]];
                } else if(complexNum[splitNum[i]]) {
                    resultNum += complexNum[splitNum[i]];
                } else {
                    console.error("Hex colors must only include hex numbers 0-9, and A-F");
                    return false;
                }
            }

            if (isPrefixed) {
                resultNum = '#' + resultNum;
            }
            return resultNum;
        }

        function remToPx(count) {
            let unit = $('html').css('font-size');

            if (typeof count !== 'undefined' && count > 0) {
                return (parseInt(unit) * count);
            } else {
                return parseInt(unit);
            }
        }

        function isOverflownHorizontally(element) {
            return element.scrollWidth > element.clientWidth;
        }

        function animateRoomName(targetElement, speed) {
            if (isOverflownHorizontally($(targetElement).parents(".info")[0])) {
                $(targetElement).animate({
                    marginLeft: "-=5"
                },
                {
                    specialEasing: { marginLeft: "linear" },
                    duration: speed,
                    complete: function() {
                        animateRoomName(this, speed);
                    }
                });
            }
        }

        function getUsersInRoom() {
            return Object.values(MPP.client.ppl);
        }

        function getHandlers(element, event) {
            return $._data($(element).get(0), "events")[event].map(e => e.handler);
        }

        function scrollToBottomRooms() {
            let more = $("#room .more");
            more.scrollTop(more.prop("scrollHeight"));
        }

        /* =================== */

        function insertRoom(room) {
            let currentSortType = GM_getValue("sortType");
            let foundRoomIdx;
            switch (currentSortType) {
                case sortType.byCreationAsc:
                    foundRoomIdx = rooms.findIndex(r => r.creationId > room.creationId);
                    break;
                case sortType.byCreationDesc:
                    foundRoomIdx = rooms.findIndex(r => r.creationId < room.creationId);
                    break;
                case sortType.byNameAsc:
                    foundRoomIdx = rooms.findIndex(r => r._id > room._id);
                    break;
                case sortType.byNameDesc:
                    foundRoomIdx = rooms.findIndex(r => r._id < room._id);
                    break;
                case sortType.byCountAsc:
                    foundRoomIdx = rooms.findIndex(r => r.count > room.count);
                    break;
                case sortType.byCountDesc:
                    foundRoomIdx = rooms.findIndex(r => r.count < room.count);
                    break;
            }

            rooms.splice(foundRoomIdx, 0, room);

            return foundRoomIdx;
        }

        function updateRoom(room) {
            let foundRoomIdx = rooms.findIndex(r => r._id == room._id);
            rooms[foundRoomIdx] = room;
            return foundRoomIdx;
        }

        function updateRoomOrder(room) {
            let currentSortType = GM_getValue("sortType");
            let oldRoomIdx = rooms.findIndex(r => r._id == room._id);
            let foundRoomIdx;

            switch (currentSortType) {
                case sortType.byCountAsc:
                    foundRoomIdx = rooms.findIndex(r => r.count > room.count);
                    break;
                case sortType.byCountDesc:
                    foundRoomIdx = rooms.findIndex(r => r.count < room.count);
                    break;
            }

            if (foundRoomIdx == -1) {
                rooms.splice(oldRoomIdx, 1);
                rooms.push(room);
            } else {
                rooms.splice(oldRoomIdx, 1);
                rooms.splice(foundRoomIdx, 0, room);
            }

            return foundRoomIdx;
        }

        function insertDomRoom(room) {
            let $roomInfo = $(roomInfoTemplate);
            $roomInfo.attr("roomname", room._id);
            $(".room-name", $roomInfo).text(room._id);

            let currentSortType = GM_getValue("sortType");
            let foundRoomIdx;
            switch (currentSortType) {
                case sortType.byCreationAsc:
                    foundRoomIdx = rooms.findIndex(r => r.creationId > room.creationId);
                    break;
                case sortType.byCreationDesc:
                    foundRoomIdx = rooms.findIndex(r => r.creationId < room.creationId);
                    break;
                case sortType.byNameAsc:
                    foundRoomIdx = rooms.findIndex(r => r._id > room._id);
                    break;
                case sortType.byNameDesc:
                    foundRoomIdx = rooms.findIndex(r => r._id < room._id);
                    break;
                case sortType.byCountAsc:
                    foundRoomIdx = rooms.findIndex(r => r.count > room.count);
                    break;
                case sortType.byCountDesc:
                    foundRoomIdx = rooms.findIndex(r => r.count < room.count);
                    break;
            }

            $("#room .more").insertAt(foundRoomIdx, $roomInfo);

            updateDomRoom(room, $roomInfo);

            return foundRoomIdx;
        }

        function appendDomRoom(room) {
            let $roomInfo = $(roomInfoTemplate);
            $roomInfo.attr("roomname", room._id);
            $(".room-name", $roomInfo).text(room._id);

            $("#room .more").append($roomInfo);

            updateDomRoom(room, $roomInfo);
        }

        function updateDomRoom(room, $roomInfo = $("#room .info[roomname=\"" + (room._id + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0') + "\"]")) {
            $(".count", $roomInfo).text(room.count);

            if(room.settings.lobby) $roomInfo.addClass("lobby");
            else $roomInfo.removeClass("lobby");
            if(!room.settings.chat) $roomInfo.addClass("no-chat");
            else $roomInfo.removeClass("no-chat");
            if(room.settings.crownsolo) $roomInfo.addClass("crownsolo");
            else $roomInfo.removeClass("crownsolo");
            if(room.settings['no cussing']) $roomInfo.addClass("no-cussing");
            else $roomInfo.removeClass("no-cussing");
            if(!room.settings.visible) $roomInfo.addClass("not-visible");
            else $roomInfo.removeClass("not-visible");
            if(room.banned) $roomInfo.addClass("banned");
            else $roomInfo.removeClass("banned");

            let initialStyle;

            // room name scrolling
            $roomInfo.hover(function() {
                initialStyle = $(".room-name", this).css("marginLeft");
                animateRoomName($(".room-name", this), 50);
            }, function() {
                $(".room-name", this).css("marginLeft", initialStyle);
                $(".room-name", this).stop();
            });
        }

        function updateDomRoomOrder(room, $roomInfo = $("#room .info[roomname=\"" + (room._id + '').replace(/[\\"']/g, '\\$&').replace(/\u0000/g, '\\0') + "\"]")) {
            let currentSortType = GM_getValue("sortType");
            let foundRoomIdx;

            switch (currentSortType) {
                case sortType.byCountAsc:
                    foundRoomIdx = rooms.findIndex(r => r.count > room.count);
                    break;
                case sortType.byCountDesc:
                    foundRoomIdx = rooms.findIndex(r => r.count < room.count);
                    break;
            }

            $roomInfo = $roomInfo.detach();
            $("#room .more").insertAt(foundRoomIdx, $roomInfo);

            return foundRoomIdx;
        }

        function sortRooms(sortingType) {
            switch (sortingType) {
                case sortType.byCreationAsc:
                    rooms.sort((a, b) => a.creationId - b.creationId);
                    break;
                case sortType.byCreationDesc:
                    rooms.sort((a, b) => b.creationId - a.creationId);
                    break;
                case sortType.byNameAsc:
                    rooms.sort((a, b) => a._id.localeCompare(b._id));
                    break;
                case sortType.byNameDesc:
                    rooms.sort((a, b) => b._id.localeCompare(a._id));
                    break;
                case sortType.byCountAsc:
                    rooms.sort((a, b) => a.count - b.count);
                    break;
                case sortType.byCountDesc:
                    rooms.sort((a, b) => b.count - a.count);
                    break;
            }
        }

        function toggleAsc($toggle) {
            $(".sort-container > *").removeClass("sort-desc");
            $(".sort-container > *").removeClass("sort-asc");
            $toggle.removeClass("sort-desc");
            $toggle.addClass("sort-asc");
        }

        function toggleDesc($toggle) {
            $(".sort-container > *").removeClass("sort-desc");
            $(".sort-container > *").removeClass("sort-asc");
            $toggle.removeClass("sort-asc");
            $toggle.addClass("sort-desc");
        }


        // replace console log
        let oldLog = unsafeWindow.console.log;

        let onLog = function(msg) {
            if (msg === "output") {
                setTimeout(() => { $(notificationMidiSelector).remove(); }, 1);
            }
        };

        unsafeWindow.console.log = function(msg) {
            if (onLog) {
                onLog(msg);
            }
            oldLog.apply(null, arguments);
        }

        // reset properties
        window.localStorage.knowsYouCanUseKeyboard = true;
        window.localStorage.gHasBeenHereBefore = true;
        window.localStorage.volume = 0;

        // hide midi connections popout
        elementReady(notificationMidiSelector).then(midiCon => {
            $(notificationMidiSelector + ' .enabled').trigger("click");

            $(notificationMidiSelector + ' ul:nth-of-type(1) li').filter(function() {
                return $(this).text() === GM_getValue("defaultMidiInput");
            }).trigger("click");
            $(notificationMidiSelector + ' ul:nth-of-type(2) li').filter(function() {
                return $(this).text() === GM_getValue("defaultMidiOutput");
            }).trigger("click");

            setTimeout(() => { $(notificationMidiSelector).remove(); }, 15);
        });

        $('#modal').hide();

        // hide notification animation
        MPP.Notification.prototype.close = function() {
            window.removeEventListener("resize", this.onresize);
            this.domElement.remove();
            this.emit("close");
        }

        // speed up animation for enter and leaving room
        MPP.client.on("participant added", function(part) {
            let nd = $(part.nameDiv);
            let cd = $(part.cursorDiv);
            nd.stop(true, true).fadeOut(0).fadeIn(200);
            cd.stop(true, true).fadeOut(0).fadeIn(200);
        });
        MPP.client.on("participant removed", function(part) {
            let nd = $(part.nameDiv);
            let cd = $(part.cursorDiv);
            cd.stop(true, true).fadeOut(200);
            nd.stop(true, true).fadeOut(200, function() {
                nd.remove();
                cd.remove();
                part.nameDiv = undefined;
                part.cursorDiv = undefined;
            });
        });

        // add ban button to users
        MPP.client.on("participant added", function(p) {
            if (!MPP.client.isOwner() || MPP.client.channel.settings.lobby || isParticipantOwner(p)) return;

            let $banButton = $(banButton).click({ participant: p }, function(e) {
                ban(e.data.participant);
            });

            elementReady(p.nameDiv).then(elem => $banButton.appendTo(elem));
        });

        MPP.client.on("participant update", function(p) {
            if (!MPP.client.isOwner() || MPP.client.channel.settings.lobby || isParticipantOwner(p) || p === null) return;

            let $banButton = $(banButton).click({ participant: p }, function(e) {
                ban(e.data.participant);
            });

            setTimeout(() => {
                if ($("#ban-button", p.nameDiv).length == 0) {
                    elementReady(p.nameDiv).then(elem => $banButton.appendTo(elem));
                }
            }, 100);
        });

        // add room controls
        let defaultRoomColor = "#273445";
        let currentRoomId = 0;

        MPP.client.on("ch", (msg) => {
            if (MPP.client.isOwner()) {
                let settings = msg.ch.settings;

                if (!$("#room-controls").length) {
                    $("body").append(roomControlsDiv);
                    settings.color = defaultRoomColor;
                    MPP.client.setChannelSettings(settings);
                }

                if (msg.ch._id !== currentRoomId) {
                    currentRoomId = msg.ch._id;
                    settings.color = defaultRoomColor;
                    MPP.client.setChannelSettings(settings);
                }

                $("#room-color").val(settings.color).change(function(e) {
                    settings.color = e.target.value;
                    MPP.client.setChannelSettings(settings);
                    $("#room-color").css("pointer-events", "none");
                    setTimeout(function() {
                        $("#room-color").css("pointer-events", "auto");
                    }, 600)
                });
            } else {
                $("#room-controls").remove();
            }
        });

        // display people count and full room titles
        $("#room").append(roomsWrapper);
        $("#room .more").appendTo("#room .rooms-wrapper");

        MPP.client._events.ls.shift();

        MPP.client.on("ls", ls => {
            let currentSortType = GM_getValue("sortType");

            if (ls.c) {
                switch (currentSortType) {
                    case sortType.byCreationAsc:
                        toggleAsc($(".sort-by-creation"));
                        break;
                    case sortType.byCreationDesc:
                        toggleDesc($(".sort-by-creation"));
                        break;
                    case sortType.byNameAsc:
                        toggleAsc($(".sort-by-name"));
                        break;
                    case sortType.byNameDesc:
                        toggleDesc($(".sort-by-name"));
                        break;
                    case sortType.byCountAsc:
                        toggleAsc($(".sort-by-count"));
                        break;
                    case sortType.byCountDesc:
                        toggleDesc($(".sort-by-count"));
                        break;
                }

                rooms.length = 0;
                ls.u.forEach(function(r, i) {
                    r.creationId = i;
                    rooms.push(r);
                });

                if (currentSortType != sortType.byCreationAsc) {
                    sortRooms(currentSortType);
                }

                rooms.forEach(appendDomRoom);
            } else {
                ls.u.forEach(room => {
                    let foundRoomIdx = rooms.findIndex(r => r._id === room._id);
                    if (foundRoomIdx == -1) {
                        insertRoom(room);
                        insertDomRoom(room);
                    } else {
                        updateRoom(room);
                        updateDomRoom(room);
//                      if (currentSortType == sortType.byCountAsc || currentSortType == sortType.byCountDesc) {
//                          updateRoomOrder(room);
//                          updateDomRoomOrder(room);
//                      }
                    }
                });
            }
        });

        // add container for buttons
        $("#bottom .relative").append(buttonContainer);
        let $buttonContainer = $("#bottom .relative .button-container");
        $("#new-room-btn").appendTo($buttonContainer);
        $("#play-alone-btn").appendTo($buttonContainer);
        $("#room-settings-btn").appendTo($buttonContainer);
        $("#midi-btn").appendTo($buttonContainer);
        $("#record-btn").appendTo($buttonContainer);
        $("#synth-btn").appendTo($buttonContainer);
        $("#sound-btn").appendTo($buttonContainer);

        // add container to volume
        $("#bottom .relative").append(volumeContainer);
        let $volumeContainer = $("#bottom .relative .volume-container");
        $("#volume").appendTo($volumeContainer);
        $("#volume-label").appendTo($volumeContainer);

        // add autoban options
        $("body").append(autobanContainer);

        $(".autoban-container #autoban-timeout").val(GM_getValue("autobanTimeout")).change(function() {
            GM_setValue("autobanTimeout", $(this).val());
        });
        $(".autoban-container #autoban-anonymous").prop("checked", GM_getValue("autobanAnonymous")).change(function() {
            GM_setValue("autobanAnonymous", this.checked);
            getUsersInRoom().forEach((user) => banIfAnon(user));
        });
        $(".autoban-container #autoban-russian-nicknames").prop("checked", GM_getValue("autobanRussianNicknames")).change(function() {
            GM_setValue("autobanRussianNicknames", this.checked);
            getUsersInRoom().forEach((user) => banIfRussianNickname(user));
        });
        $(".autoban-container #autoban-regex").val(GM_getValue("autobanRegex")).change(function() {
            GM_setValue("autobanRegex", $(this).val());
            getUsersInRoom().forEach((user) => banIfRegexMatched(user, $(this).val()));
        });

        MPP.client.on("participant added", autobanUser);
        MPP.client.on("participant update", autobanUser);

        // manipulate events in order for text input to work
        let keydownHandlers = [];
        let keyupHandlers = [];
        let keypressHandlers = [];

        $(".autoban-container input[type='text'], .autoban-container input[type='number']").focus(function(evt) {
            getHandlers(document, "keydown").forEach((handler) => keydownHandlers.push(handler));
            getHandlers(document, "keyup").forEach((handler) => keyupHandlers.push(handler));
            getHandlers(unsafeWindow, "keypress").forEach((handler) => keypressHandlers.push(handler));

            $(document).off("keydown");
            $(document).off("keyup");
            $(unsafeWindow).off("keypress");
        });

        $(".autoban-container input[type='text'], .autoban-container input[type='number']").blur(function(evt) {
            keydownHandlers.forEach((handler) => $(document).on("keydown", handler));
            keyupHandlers.forEach((handler) => $(document).on("keyup", handler));
            keypressHandlers.forEach((handler) => $(unsafeWindow).on("keypress", handler));

            keydownHandlers = [];
            keyupHandlers = [];
            keypressHandlers = [];
        });

        // add sorting options to room list
        let roomClickHandler = getHandlers($("#room")[0], "click")[0];
        let documentMousedownHandler = () => {};
        $("#room").off("click");

        $("#room").click(function(event) {
            if (!$(event.target).hasClass("sort-toggle")) {
                roomClickHandler(event);

                $("#room .more .new").remove();

                setTimeout(scrollToBottomRooms, 150);

                let handlers = getHandlers(document, "mousedown");
                for (const handler of handlers) {
                    if (handler.name === "doc_click") {
                        documentMousedownHandler = handler;
                        $(document).off("mousedown", handler);
                    }
                }
            }
        });

        $(document).on("mousedown", function(event) {
            if (!$(event.target).hasClass("sort-toggle")) {
                documentMousedownHandler(event);
            }
        });


        $("#room .more").prepend(sortContainerTemplate);
        $(".sort-toggle").click(function(event) {
            let $toggle = $(this);
            if ($toggle.hasClass("sort-asc")) {
                toggleDesc($toggle);
            } else {
                toggleAsc($toggle);
            }

            let chosenSortType;

            if ($(this).is(".sort-by-creation.sort-asc")) {
                chosenSortType = sortType.byCreationAsc;
            }
            if ($(this).is(".sort-by-creation.sort-desc")) {
                chosenSortType = sortType.byCreationDesc;
            }
            if ($(this).is(".sort-by-name.sort-asc")) {
                chosenSortType = sortType.byNameAsc;
            }
            if ($(this).is(".sort-by-name.sort-desc")) {
                chosenSortType = sortType.byNameDesc;
            }
            if ($(this).is(".sort-by-count.sort-asc")) {
                chosenSortType = sortType.byCountAsc;
            }
            if ($(this).is(".sort-by-count.sort-desc")) {
                chosenSortType = sortType.byCountDesc;
            }

            GM_setValue("sortType", chosenSortType);

            sortRooms(chosenSortType);

            $("#room .more .info").remove();

            rooms.forEach(appendDomRoom);
        });
    });

    return {
        rooms,
        get defaultMidiInput() {
            return GM_getValue("defaultMidiInput");
        },
        set defaultMidiInput(value) {
            GM_setValue("defaultMidiInput", value);
        },

        get defaultMidiOutput() {
            return GM_getValue("defaultMidiOutput");
        },
        set defaultMidiOutput(value) {
            GM_setValue("defaultMidiOutput", value);
        }
    }
})();