/*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 .*/ /** * Class representing the settings panel * @extends panelObj */ class pmPanel extends panelObj{ /** * Instantiates a new Panel Object * @param {channel} client - Parent client Management Object * @param {Document} panelDocument - Panel Document */ constructor(client, panelDocument, startSesh){ super(client, "Private Messaging", "/panel/pm", panelDocument); /** * String to hold name of currently active sesh */ this.activeSesh = ""; /** * Random UUID to identify the panel with the pmHandler */ this.uuid = crypto.randomUUID(); /** * PM TX Sound */ this.txSound = '/nonfree/imsend.ogg'; /** * Message Buffer Scroll Top on last scroll */ this.lastPos = 0; /** * Height of Message Buffer on last scroll */ this.lastHeight = 0; /** * Width of Message Buffer on last scroll */ this.lastWidth = 0; /** * Whether or not auto-scroll is enabled */ this.autoScroll = true; /** * re-occuring auto-scroll call * cope for the fact that postprocessedMessage objects in renderMessage aren't throwing load events */ this.scrollInterval = setInterval(this.handleAutoScroll.bind(this), 200); //Tell PMHandler to start tracking this panel this.client.pmHandler.panelList.set(this.uuid, null); //Define network related listeners this.defineListeners(); //If a start sesh was provided if(startSesh != null && startSesh != ""){ //Send message out to server this.client.pmSocket.emit("pm", { recipients: startSesh.split(" "), msg: "" }); } } closer(){ //Tell PMHandler to stop tracking this panel this.client.pmHandler.panelList.delete(this.uuid); //Clear the scroll interval clearInterval(this.scrollInterval); //Run derived closer super.closer(); } async docSwitch(){ //Call derived method super.docSwitch(); this.startSeshButton = this.panelDocument.querySelector('#pm-panel-start-sesh'); this.seshList = this.panelDocument.querySelector('#pm-panel-sesh-list'); this.seshBuffer = this.panelDocument.querySelector('#pm-panel-sesh-buffer'); this.seshPrompt = this.panelDocument.querySelector('#pm-panel-message-prompt'); this.seshSendButton = this.panelDocument.querySelector('#pm-panel-send-button'); //reset auto-scroll this.autoScroll = true; this.setupInput(); this.renderSeshList(); //If we have an active sesh if(this.activeSesh != null && this.activeSesh != ""){ //Render messages this.renderMessages(); } } /** * Defines network related event listeners */ defineListeners(){ this.client.pmSocket.on("message", this.handlePM.bind(this)); this.client.pmSocket.on("sent", this.handlePM.bind(this)); } /** * Defines input-related event handlers */ setupInput(){ this.startSeshButton.addEventListener('click', this.startSesh.bind(this)); this.seshPrompt.addEventListener("keydown", this.send.bind(this)); this.seshSendButton.addEventListener("click", this.send.bind(this)); this.seshBuffer.addEventListener('scroll', this.scrollHandler.bind(this)); this.ownerDoc.defaultView.addEventListener('resize', this.handleAutoScroll.bind(this)); } startSesh(event){ new startSeshPopup(event, this.client, this.renderSeshList.bind(this), this.ownerDoc); } handlePM(data){ const nameObj = pmHandler.genSeshName(data); //If this message is for the active sesh if(nameObj.name == this.activeSesh){ //Render out the newest message this.renderMessage(data); }else{ //Re-render out the sesh list this.renderSeshList(); } } /** * sends private message from sesh prompt to server * @param {Event} event - Event passed down from Event Handler */ send(event){ if((!event || !event.key || event.key == "Enter") && this.seshPrompt.value && this.activeSesh != ''){ //Pull current sesh from sesh list const sesh = this.client.pmHandler.seshList.get(this.activeSesh); //Preprocess message from prompt const preprocessedMessage = this.client.chatBox.commandPreprocessor.preprocess(this.seshPrompt.value); //If preprocessedMessage had it's send flag thrown as false if(preprocessedMessage != false){ //Stick recipients into the pre-processed message preprocessedMessage.recipients = sesh.recipients; //Send message out to server this.client.pmSocket.emit("pm", preprocessedMessage); if(localStorage.getItem('txPMSound') == 'true'){ utils.ux.playSound(this.txSound); } } //Clear our prompt this.seshPrompt.value = ""; } } /** * Render out current sesh array to sesh list UI */ renderSeshList(){ //If we don't have a sesh list if(this.seshList == null){ //Fuck off, you're not even done building the object yet. return; } //Clear out the sesh list this.seshList.innerHTML = ""; //Assemble temporary array from client PM Handler sesh list const seshList = Array.from(this.client.pmHandler.seshList); //If we have existing sessions and no active sesh if(this.activeSesh == "" && seshList[0] != null){ //Enable UI elements this.seshPrompt.disabled = false; this.seshSendButton.disabled = false; //Render out messages this.renderMessages(); //Set the first one as active this.activeSesh = seshList[0][1].id; //Tell PMHandler what sesh we have open for notification reasons this.client.pmHandler.readSesh(this.uuid, this.activeSesh); } //For each session tracked by the pmHandler for(const seshEntry of seshList){ this.renderSeshListEntry(seshEntry[1]); } } /** * Renders out a given messaging sesh to the sesh list UI */ renderSeshListEntry(sesh){ //Create container div const entryDiv = document.createElement('div'); //Set conatiner div classes entryDiv.classList.add('pm-panel-sesh-list-entry','interactive'); //Set dataset sesh name entryDiv.dataset.id = sesh.id; //If the current entry is the active sesh if(sesh.id == this.activeSesh){ //mark it as such entryDiv.classList.add('positive'); //If it contains something unread }else if(sesh.unread){ entryDiv.classList.add('positive-afterglow'); } //Create sesh label const seshLabel = document.createElement('p'); //Create human-readable label out of members array seshLabel.textContent = utils.unescapeEntities(sesh.id); //append sesh label to entry div entryDiv.appendChild(seshLabel); //Append entry div to sesh list this.seshList.appendChild(entryDiv); //Add input-related event listener for entry div entryDiv.addEventListener('click', this.selectSesh.bind(this)); } selectSesh(event){ //Attempt to pull previously active sesh item from list const wasActive = this.panelDocument.querySelector('.positive'); //If there was an active sesh if(wasActive != null){ //Remove active sesh class from old item wasActive.classList.remove('positive'); } //Pull new active sesh name from target dataset this.activeSesh = event.target.dataset.id; //Set new sesh as active sesh event.target.classList.add('positive'); //Tell PMHandler what sesh we have open for notification reasons this.client.pmHandler.readSesh(this.uuid, this.activeSesh); //Reset auto scroll to scroll newly selected sesh down to the bottom this.autoScroll = true; //Re-render message buffer this.renderMessages(); //Re-Render Sesh List this.renderSeshList(); } renderMessages(){ //Empty out the sesh buffer this.seshBuffer.innerHTML = ""; //Pull sesh from pmHandler const sesh = this.client.pmHandler.seshList.get(this.activeSesh); //If the sesh is real if(sesh != null){ //for each message in the sesh for(const message of sesh.messages){ //Render out messages to the buffer this.renderMessage(message); } } } /** * Renders message out to PM Panel Message Buffer * @param {Object} message - Message to render */ async renderMessage(message){ //If we have an empty message if(message.msg == null || message.msg == ''){ //BAIL!! return; } //Run postprocessing functions on chat message const postprocessedMessage = client.chatBox.chatPostprocessor.postprocess(message, true); //Append message to buffer this.seshBuffer.appendChild(postprocessedMessage); //Auto-scroll buffer on content load this.handleAutoScroll(); } /** * Handles scrolling within the message buffer * @param {Event} event - Event passed down from Event Handler */ scrollHandler(event){ //If we're just starting out if(this.lastPos == 0){ //Set last pos for the first time this.lastPos = this.seshBuffer.scrollTop; } //Calculate scroll delta const deltaY = this.seshBuffer.scrollTop - this.lastPos; //Grab visible bounding rect so we don't have to do it again (can't use offset because someone might zoom in :P) const bufferRect = this.seshBuffer.getBoundingClientRect(); const bufferHeight = Math.round(bufferRect.height); const bufferWidth = Math.round(bufferRect.width); //If last height was unset if(this.lastHeight == 0){ //Set it based on buffer Height this.lastHeight = bufferHeight; } //if last width is unset if(this.lastWidth == 0){ //Set it based on buffer width this.lastWidth = bufferWidth; } //If we're scrolling up if(deltaY < 0){ //If we have room to scroll, and we didn't resize if(this.seshBuffer.scrollHeight > bufferHeight && (this.lastWidth == bufferWidth && this.lastHeight == bufferHeight)){ //Disable auto scrolling this.autoScroll = false; //We probably resized }else{ this.handleAutoScroll(); } //Otherwise if the difference between the message buffers scroll height and offset height is equal to the scroll top //(Because it is scrolled all the way down) }else if((this.seshBuffer.scrollHeight - bufferHeight) == this.seshBuffer.scrollTop){ this.autoScroll = true; } //Set last post/size for next the run this.lastPos = this.seshBuffer.scrollTop; this.lastHeight = bufferHeight; this.lastWidth = bufferWidth; } /** // * Auto-scrolls sesh chat buffer when new chats are entered. */ handleAutoScroll(){ //If autoscroll is enabled if(this.autoScroll){ //Set seshBuffer scrollTop to the difference between scrollHeight and buffer height (scroll to the bottom) this.seshBuffer.scrollTop = this.seshBuffer.scrollHeight - Math.round(this.seshBuffer.getBoundingClientRect().height); } } } /** * Class representing pop-up dialogue to start a private messaging sesh */ class startSeshPopup{ /** * Instantiates a new schedule media Pop-up * @param {Event} event - Event passed down from Event Listener * @param {channel} client - Parent Client Management Object * @param {String} url - URL/link to media to queue * @param {String} title - Title of media to queue * @param {Function} cb - Callback function, passed upon pop-up creation * @param {Document} doc - Current owner documnet of the panel, so we know where to drop our pop-up */ constructor(event, client, cb, doc){ /** * Parent Client Management Object */ this.client = client; /** * Callback function, passed upon pop-up creation */ this.cb = cb; //Create media popup and call async constructor when done //unfortunately we cant call constructors asyncronously, and we cant call back to this from super, so we can't extend this as it stands :( /** * canopyUXUtils.popup() object */ this.popup = new canopyUXUtils.popup('/startChatSesh', true, this.asyncConstructor.bind(this), doc); } /** * Continuation of object construction, called after child popup object construction */ asyncConstructor(){ //Grab required UI elements this.startSeshButton = this.popup.contentDiv.querySelector('#pm-sesh-popup-button'); this.usernamePrompt = this.popup.contentDiv.querySelector('#pm-sesh-popup-prompt'); //Setup input this.setupInput(); } /** * Defines input-related Event Handlers */ setupInput(){ //Setup input this.startSeshButton.addEventListener('click', this.startSesh.bind(this)); this.popup.popupDiv.addEventListener('keydown', this.startSesh.bind(this)); } /** * Handles sending request to schedule item to the queue * @param {Event} event - Event passed down from Event Listener */ startSesh(event){ //If we clicked or hit enter if(event.key == null || event.key == "Enter"){ //Send message out to server this.client.pmSocket.emit("pm", { recipients: this.usernamePrompt.value.split(" "), msg: "" }); //If we have a function if(typeof this.cb == "function"){ //Call any callbacks we where given this.cb(); } //Close the popup this.popup.closePopup(); } } }