/*Canopy - The next generation of stoner streaming software Copyright (C) 2024-2026 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 .*/ /** * 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); /** * List of currently connected users */ this.userList = []; /** * 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 connected and buffered usernames to assigned username color/flair */ this.colorMap = new Map(); /** * users div */ this.userDiv = document.querySelector("#chat-panel-users-div"); /** * userlist div */ this.userListDiv = 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([]); }) } /** * Randomly assigns and gets a color to usernames that aren't on the colormap * @param {String} user username to assign a color */ assignColor(user){ //if this user wasn't in the previous colormap if(this.colorMap.get(user) == null){ //randomly pick a color let color = this.userColors[Math.floor(Math.random()*this.userColors.length)] //Store new randomly selected color this.colorMap.set(user, color); //Return color return color; }else{ //Return color from previous entry return this.colorMap.get(user); } } /** * 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.userListDiv.innerHTML = null; //Set userlist array to received list this.userList = list; //for each user list.forEach((user) => { //Assign color and render user this.renderUser(user, this.assignColor(user.user)); }); //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.userListDiv.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}`))}], ["PM", ()=>{this.client.cPanel.setActivePanel(new pmPanel(client, undefined, 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){ let ignored = false; //For each douchebag in the ignored users list for(const douchebag of this.client.user.ignored){ if(douchebag.user == user.user){ /* Normally I abhor making if statements to set booleans, but this only need to happen when its true Plus I also needed to put the break statement somewhere The fucked thing is the break statement isnt necissary with the if statement, but without it it becomes necissary But it cant be used without the if statement, making the task of reducing linecount for this definition a fucking sisyphean nightmare */ ignored = true; break; } } if(ignored){ menuMap.set("Unignore", ()=>{this.client.chatBox.transmit(`!unignore ${user.user}`);}); }else{ menuMap.set("Ignore", ()=>{this.client.chatBox.transmit(`!ignore ${user.user}`);}); } if(this.client.user.permMap.chan.get("kickUser")){ menuMap.set("Kick", ()=>{this.client.chatBox.transmit(`!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"); } } //returns list of strings containing all currently online users getOnlineUserNames(){ var names = []; for(let user of this.userList){ names.push(user.user); } return names; } }