455 lines
15 KiB
JavaScript
455 lines
15 KiB
JavaScript
/*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 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){
|
|
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);
|
|
|
|
this.defineListeners();
|
|
}
|
|
|
|
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(){
|
|
//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){
|
|
console.log("SCROLLME");
|
|
//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();
|
|
}
|
|
}
|
|
} |