Source: userlist.js

/*Canopy - The next generation of stoner streaming software
Copyright (C) 2024-2025 Rainbownapkin and the TTN Community

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <https://www.gnu.org/licenses/>.*/

/**
 * Class containing logic behind userlist UX
 */
class userList{
    /**
     * Instantiates a new userList object
     * @param {channel} client - Parent client mgmt object
     */
    constructor(client){
        /**
         * Parent Client Management object
         */
        this.client = client

        /**
         * Click Dragger object for handling userlist resizes
         */
        this.clickDragger = new canopyUXUtils.clickDragger("#chat-panel-users-drag-handle", "#chat-panel-users-div", true, this.client.chatBox.clickDragger);
        
        /**
         * Userlist color array (Maps to css classes)
         */
        this.userColors = [
            "userlist-color0",
            "userlist-color1",
            "userlist-color2",
            "userlist-color3", 
            "userlist-color4",
            "userlist-color5",
            "userlist-color6"];
        
        /**
         * Map of usernames to assigned username color
         */
        this.colorMap = new Map();

        /**
         * users div
         */
        this.userDiv = document.querySelector("#chat-panel-users-div");

        /**
         * userlist div
         */
        this.userList = document.querySelector("#chat-panel-users-list-div");

        /**
         * user count label
         */
        this.userCount = document.querySelector("#chat-panel-user-count");

        /**
         * userlist toggle button
         */
        this.toggleIcon = document.querySelector("#chat-panel-users-toggle");

        //Call setup functions
        this.setupInput();
        this.defineListeners();
    }

    /**
     * Defines input-related event listeners
     */
    setupInput(){
        this.toggleIcon.addEventListener("click", ()=>{this.toggleUI()});
        this.userCount.addEventListener("click", ()=>{this.toggleUI()});
    }

    /**
     * Defines network-related event listeners
     */
    defineListeners(){
        this.client.socket.on('userList', (data) => {
            this.updateList(data);
        });

        this.client.socket.on("disconnect", () => {
            this.updateList([]);
        })
    }

    /**
     * Updates UX after user list change
     * @param {Array} list - Userlist data from server
     */
    updateList(list){
        //Clear list and set user count
        this.userCount.textContent = list.length == 1 ? '1 User' : `${list.length} Users`;
        this.userList.innerHTML = null;

        //create a new map
        var newMap = new Map();

        //for each user
        list.forEach((user) => {
            //randomly pick a color
            var color = this.userColors[Math.floor(Math.random()*this.userColors.length)]

            //if this user was in the previous colormap
            if(this.colorMap.get(user.user) != null){
                //Override with previous color
                color = this.colorMap.get(user.user);
            }

            newMap.set(user.user, color);
            this.renderUser(user, color);
        });

        this.colorMap = newMap;

        //Make sure we're not cutting the ux off
        this.clickDragger.fixCutoff();
    }

    /**
     * Renders out a single username to the userlist
     * @param {String} user - Username to render
     * @param {String} flair - Flair to render as
     */
    renderUser(user, flair){

        //Create user span
        var userSpan = document.createElement('span');
        userSpan.classList.add('chat-panel-users', 'user-entry');

        //Create high-level label
        var highLevel = document.createElement('p');
        highLevel.classList.add("user-list-high-level","high-level");
        highLevel.textContent = `${user.highLevel}`;

        //Create nameplate
        var userEntry = document.createElement('p');
        userEntry.innerText = user.user;
        userEntry.id = `user-entry-${user.user}`;

        //Override color with flair
        if(user.flair != "classic"){
            flair = `flair-${user.flair}`;
        }
        //Add classes to classList
        userEntry.classList.add("chat-panel-users","user-entry",flair); 

        //Add high-level username to nameplate
        userSpan.appendChild(highLevel);
        userSpan.appendChild(userEntry);

        //Setup profile tooltip
        userSpan.addEventListener('mouseenter',(event)=>{utils.ux.displayTooltip(event, `profile?user=${user.user}`, true, null, true);});

        //Setup profile context menu
        userSpan.addEventListener('click', renderContextMenu.bind(this));
        userSpan.addEventListener('contextmenu', renderContextMenu.bind(this));

        this.userList.appendChild(userSpan);

        function renderContextMenu(event){
            //Setup menu map
            let menuMap = new Map([
                    ["Profile", ()=>{this.client.cPanel.setActivePanel(new panelObj(this.client, `${user.user}`, `/panel/profile?user=${user.user}`))}],
                    ["Mention", ()=>{this.client.chatBox.catChat(`${user.user} `)}],
                    ["Toke With", ()=>{this.client.chatBox.tokeWith(user.user)}],
            ]);

            if(user.user != "Tokebot" && user.user != this.client.user.user){
                if(this.client.user.permMap.chan.get("kickUser")){
                    menuMap.set("Kick", ()=>{this.client.chatBox.commandPreprocessor.preprocess(`!kick ${user.user}`)});
                }

                if(this.client.user.permMap.chan.get("banUser")){
                    menuMap.set("Channel Ban", ()=>{new chanBanUserPopup(this.client.channelName, user.user);});
                }

                if(this.client.user.permMap.site.get("banUser")){
                    menuMap.set("Site Ban", ()=>{new banUserPopup(user.user);});
                }
            }

            //Display the menu
            utils.ux.displayContextMenu(event, user.user, menuMap);
        }
    }

    toggleUI(show = !this.userDiv.checkVisibility()){
        localStorage.setItem("userlistHidden", !show);

        if(show){
            this.userDiv.style.display = "flex";
            this.toggleIcon.classList.replace("bi-caret-left-fill","bi-caret-down-fill");
            this.clickDragger.fixCutoff();
        }else{
            this.userDiv.style.display = "none";
            this.toggleIcon.classList.replace("bi-caret-down-fill","bi-caret-left-fill");
        }
    }

}