Source: panels/emotePanel.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 representing Emote Panel UX
 * @extends panelObj
 */
class emotePanel extends panelObj{
    /**
     * Instantiates a new Panel Object
     * @param {channel} client - Parent client Management Object
     * @param {Document} panelDocument - Panel Document
     */
    constructor(client, panelDocument){
        super(client, "Emote Palette", "/panel/emote", panelDocument);

        this.client.socket.on("personalEmotes", this.renderEmoteLists.bind(this));
    }

    closer(){
        this.client.socket.off("personalEmotes", this.renderEmoteLists.bind(this));
    }

    docSwitch(){
        this.siteEmoteTitle = this.panelDocument.querySelector('#site-emotes-title');
        this.chanEmoteTitle = this.panelDocument.querySelector('#chan-emotes-title');
        this.personalEmoteTitle = this.panelDocument.querySelector('#personal-emotes-title');

        this.siteEmoteToggle = this.panelDocument.querySelector('#site-emotes-toggle');
        this.chanEmoteToggle = this.panelDocument.querySelector('#chan-emotes-toggle');
        this.personalEmoteToggle = this.panelDocument.querySelector('#personal-emotes-toggle');

        this.siteEmoteList = this.panelDocument.querySelector('#emote-panel-site-list');
        this.chanEmoteList = this.panelDocument.querySelector('#emote-panel-chan-list');
        this.personalEmoteSection = this.panelDocument.querySelector('#emote-panel-personal-section');
        this.personalEmoteList = this.panelDocument.querySelector('#emote-panel-personal-list');

        this.searchPrompt = this.panelDocument.querySelector('#emote-panel-search-prompt');

        this.personalEmoteLinkPrompt = this.panelDocument.querySelector('#new-emote-link-input');
        this.personalEmoteNamePrompt = this.panelDocument.querySelector('#new-emote-name-input');
        this.personalEmoteAddButton = this.panelDocument.querySelector('#new-emote-button');

        this.setupInput();

        this.renderEmoteLists();
    }

    /**
     * Defines input-related event handlers
     */
    setupInput(){
        //Make sure to remove any event listeners in-case we moving an already instantiated panel
        this.siteEmoteToggle.removeEventListener("click", this.toggleSiteEmotes.bind(this));
        this.siteEmoteToggle.addEventListener("click", this.toggleSiteEmotes.bind(this));

        this.chanEmoteToggle.removeEventListener("click", this.toggleChanEmotes.bind(this));
        this.chanEmoteToggle.addEventListener("click", this.toggleChanEmotes.bind(this));

        this.personalEmoteToggle.removeEventListener("click", this.togglePersonalEmotes.bind(this));
        this.personalEmoteToggle.addEventListener("click", this.togglePersonalEmotes.bind(this));

        this.siteEmoteTitle.removeEventListener("click", this.toggleSiteEmotes.bind(this));
        this.siteEmoteTitle.addEventListener("click", this.toggleSiteEmotes.bind(this));

        this.chanEmoteTitle.removeEventListener("click", this.toggleChanEmotes.bind(this));
        this.chanEmoteTitle.addEventListener("click", this.toggleChanEmotes.bind(this));

        this.personalEmoteTitle.removeEventListener("click", this.togglePersonalEmotes.bind(this));
        this.personalEmoteTitle.addEventListener("click", this.togglePersonalEmotes.bind(this));

        this.searchPrompt.removeEventListener('keyup', this.renderEmoteLists.bind(this));
        this.searchPrompt.addEventListener('keyup', this.renderEmoteLists.bind(this));

        this.personalEmoteAddButton.removeEventListener("click", this.addPersonalEmote.bind(this));
        this.personalEmoteAddButton.addEventListener("click", this.addPersonalEmote.bind(this));
    }

    /**
     * Toggles Site emote display
     * @param {Event} event - Event passed down by event listener
     */
    toggleSiteEmotes(event){
        this.toggleEmotes(this.siteEmoteToggle, this.siteEmoteList);
    }

    /**
     * Toggles Channel emote display
     * @param {Event} event - Event passed down by event listener
     */
    toggleChanEmotes(event){
        this.toggleEmotes(this.chanEmoteToggle, this.chanEmoteList);
    }

    /**
     * Toggles Personal emote display
     * @param {Event} event - Event passed down by event listener
     */
    togglePersonalEmotes(event){
        this.toggleEmotes(this.personalEmoteToggle, this.personalEmoteSection);
    }

    /**
     * Toggles a specified emote list on or off
     * @param {Node} icon - Toggle Icon for given list
     * @param {Node} list - Emote list container to toggle
     */
    toggleEmotes(icon, list){
        if(list.checkVisibility()){
            icon.classList.replace('bi-caret-down-fill','bi-caret-left-fill');
            list.style.display = 'none';
        }else{
            icon.classList.replace('bi-caret-left-fill', 'bi-caret-down-fill');
            list.style.display = 'grid';
        }
    }

    /**
     * Concatenates specified emote into chat prompt input
     * @param {String} emote - Emote to concat into chat
     */
    useEmote(emote){
        //If we're using this from the active panel
        if(this.client.cPanel.activePanel == this){
            //Close it
            this.client.cPanel.hideActivePanel();
        }

        //Add the emote to the chatbox prompt
        this.client.chatBox.catChat(`[${emote}]`);
    }

    /**
     * Requests server to add emote to list of personal emotes
     * @param {Event} event - Event passed down by event listener
     */
    addPersonalEmote(event){
        //Collect input
        const name = this.personalEmoteNamePrompt.value;
        const link = this.personalEmoteLinkPrompt.value;

        //Empty out prompts
        this.personalEmoteNamePrompt.value = '';
        this.personalEmoteLinkPrompt.value = '';

        //Send emote to server
        this.client.socket.emit("addPersonalEmote", {name, link});
    }

    /**
     * Requests server to remove emote from list of personal emotes
     * @param {String} name - Name of emote to delete
     */
    deletePersonalEmote(name){
        //send out delete
        this.client.socket.emit('deletePersonalEmote', {name});
    }

    /**
     * Renders out emote list to panel document
     */
    renderEmoteLists(){
        //if we've initialized the search prompt (wont happen yet first run)
        if(this.searchPrompt != null){
            //Get the search value
            var search = this.searchPrompt.value;
        }

        //pull emote lists from the command preprocessor
        var siteEmotes = this.client.chatBox.commandPreprocessor.emotes.site;
        var chanEmotes = this.client.chatBox.commandPreprocessor.emotes.chan;
        var personalEmotes = this.client.chatBox.commandPreprocessor.emotes.personal;

        //If we have a search bar and a search in the search bar 
        if(search != null && search != ''){
            //filter emote lists using the filterQuery function
            siteEmotes = siteEmotes.filter(filterQuery);
            chanEmotes = chanEmotes.filter(filterQuery);
            personalEmotes = personalEmotes.filter(filterQuery);

            function filterQuery(emote){
                //return true for anyany case-insensitive matches
                return (emote.name.toLowerCase().match(search.toLowerCase())) != null;
            }
        }

        //render out the emote lists
        this.renderEmotes(siteEmotes, this.siteEmoteList);
        this.renderEmotes(chanEmotes, this.chanEmoteList);
        this.renderEmotes(personalEmotes, this.personalEmoteList, true);
    }

    /**
     * Renders out emotes to emote lists
     * @param {Array} emoteList - list of emotes to render
     * @param {Node} container - Container to render emotes out to
     * @param {Boolean} personal - Denotes whether or not we're rendering personal emotes
     */
    renderEmotes(emoteList, container, personal = false){
        //Clear out the container
        container.innerHTML = '';

        //If we have two or less emotes
        if(emoteList.length <= 2){
            //Set the container display to flex
            container.style.display = 'flex';
        //otherwise
        }else{
            //Set the container display to grid
            container.style.display = 'grid';
        }

        //For each emote
        emoteList.forEach((emote) => {
            //Create div to hold emote span
            const emoteDiv = document.createElement('div');
            emoteDiv.classList.add('emote-panel-list-emote');

            const emoteSpan = document.createElement('span');
            emoteSpan.classList.add('emote-panel-list-emote');

            //If we have a low emote count
            if(emoteList.length <= 2){
                //render them huuuuuge
                emoteDiv.classList.add('emote-panel-list-big-emote');
                emoteSpan.classList.add('emote-panel-list-big-emote');
            }

            //If the emote is an image
            if(emote.type == 'image'){
                //Create image node
                var emoteMedia = document.createElement('img');
            //if emote is a video
            }else if(emote.type == 'video'){
                //create video node
                var emoteMedia = document.createElement('video');
                //Set video properties
                emoteMedia.autoplay = true;
                emoteMedia.muted = true;
                emoteMedia.controls = false;
                emoteMedia.loop = true;
            }

            //set media link as source
            emoteMedia.src = emote.link;
            //Set media class
            emoteMedia.classList.add('emote-list-media');

            //if we have a low emote count
            if(emoteList.length <= 2){
                //render them huuuuuge
                emoteMedia.classList.add('emote-list-big-media');
            }


            //Create paragraph tag
            const emoteTitle = document.createElement('p');
            //Set title class
            emoteTitle.classList.add('emote-list-title');
            //Set emote title
            emoteTitle.textContent = utils.unescapeEntities(`[${emote.name}]`);

            //if we're rendering personal emotes
            if(personal){
                //create span to hold trash icon
                const trashSpan = document.createElement('span');
                trashSpan.classList.add('emote-list-trash-icon');

                //Create trash icon
                const trashIcon = document.createElement('i');
                trashIcon.classList.add('emote-list-trash-icon', 'bi-trash-fill');
                trashIcon.id = `emote-list-trash-icon-${emote.name}`;

                //add deletePersonalEmote event listener
                trashIcon.addEventListener('click', ()=>{this.deletePersonalEmote(emote.name)});
                
                //Add trash icon to trash span
                trashSpan.appendChild(trashIcon);

                //append trash span to emote div
                emoteDiv.appendChild(trashSpan);
            }
            
            //Add the emote media to the emote span
            emoteSpan.appendChild(emoteMedia);
            //Add title paragraph node
            emoteSpan.appendChild(emoteTitle);

            //Add useEmote event listener
            emoteSpan.addEventListener('click', ()=>{this.useEmote(emote.name)});

            //Add emote span to the emote div
            emoteDiv.appendChild(emoteSpan);

            //Append the mote span to the emote list
            container.appendChild(emoteDiv);
        })
    }
}