Basic chat UI complete.

This commit is contained in:
rainbow napkin 2025-10-01 04:33:24 -04:00
parent f109314163
commit e81a4c0973
11 changed files with 393 additions and 71 deletions

View file

@ -150,17 +150,14 @@ class pmHandler{
* @returns {String} sanatized/validates message, returns null on validation failure * @returns {String} sanatized/validates message, returns null on validation failure
*/ */
sanatizeMessage(msg){ sanatizeMessage(msg){
//if msg is empty or null //Normally I'd kill empty messages here
if(msg == null || msg == ''){ //But instead we're allowing them for sesh startups
//Pimp slap that shit into fucking oblivion
return null;
}
//Trim and Sanatize for XSS //Trim and Sanatize for XSS
msg = validator.trim(validator.escape(msg)); msg = validator.trim(validator.escape(msg));
//Return whether or not the shit was long enough //Return whether or not the shit was too long
if(validator.isLength(msg, {min: 1, max: 255})){ if(validator.isLength(msg, {min: 0, max: 255})){
//If it's valid return the message //If it's valid return the message
return msg; return msg;
} }

View file

@ -26,11 +26,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>. %>
</div> </div>
<div id="pm-panel-sesh-container"> <div id="pm-panel-sesh-container">
<div id="pm-panel-sesh-buffer"> <div id="pm-panel-sesh-buffer">
<div id="pm-panel-sesh-welcome">
<h1>Start a sesh to start chatting!</h1>
</div>
</div> </div>
<div class="control-prompt" id="pm-panel-sesh-control-div"> <div class="control-prompt" id="pm-panel-sesh-control-div">
<input class="control-prompt" id="pm-panel-message-prompt" placeholder="Chat..."> <input class="control-prompt" id="pm-panel-message-prompt" placeholder="Chat..." disabled>
<button class="positive-button" id="pm-panel-send-button">Send</button> <button class="positive-button" id="pm-panel-send-button" disabled>Send</button>
</div> </div>
</div> </div>
</div> </div>

View file

@ -61,8 +61,32 @@ div.pm-panel-sesh-list-entry{
flex-direction: row; flex-direction: row;
} }
div.pm-panel-sesh-list-entry p{
pointer-events: none;
}
div.pm-panel-sesh-list-entry, div.pm-panel-sesh-list-entry p{ div.pm-panel-sesh-list-entry, div.pm-panel-sesh-list-entry p{
margin: 0; margin: 0;
text-wrap: nowrap; text-wrap: nowrap;
text-align: center; text-align: center;
}
#pm-panel-sesh-buffer span{
display: flex;
flex-direction: row;
margin: 0;
}
.pm-panel-sesh-message-sender, .pm-panel-sesh-message-content{
margin: 0;
font-size: 10pt;
}
#pm-panel-sesh-welcome{
display: flex;
flex-direction: column;
justify-content: center;
text-align: center;
height: 100%;
} }

View file

@ -0,0 +1,27 @@
/*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/>.*/
#pm-sesh-popup-div{
display: flex;
}
#pm-sesh-popup-div p{
margin: 0;
}
#pm-sesh-popup-sup{
font-size: 0.7em
}

View file

@ -129,13 +129,13 @@ button{
border-radius: 0.5em; border-radius: 0.5em;
} }
button:hover{ button:hover:not([disabled]){
color: var(--focus0-alt1); color: var(--focus0-alt1);
background-color: var(--focus0-alt0); background-color: var(--focus0-alt0);
box-shadow: var(--focus-glow0); box-shadow: var(--focus-glow0);
} }
button:active{ button:active:not([disabled]){
color: var(--focus0-alt0); color: var(--focus0-alt0);
background-color: var(--focus0-alt1); background-color: var(--focus0-alt1);
box-shadow: var(--focus-glow0-alt0); box-shadow: var(--focus-glow0-alt0);
@ -179,13 +179,13 @@ textarea{
color: var(--accent1); color: var(--accent1);
} }
.danger-button:hover, .critical-danger-button, .critical-danger-button:hover{ .danger-button:hover:not([disabled]), .critical-danger-button, .critical-danger-button:hover{
background-color: var(--danger0-alt1); background-color: var(--danger0-alt1);
color: var(--danger0-alt0); color: var(--danger0-alt0);
box-shadow: var(--danger-glow0); box-shadow: var(--danger-glow0);
} }
.critical-danger-button:hover{ .critical-danger-button:hover:not([disabled]){
background-color: var(--danger0-alt2); background-color: var(--danger0-alt2);
} }
@ -219,12 +219,12 @@ textarea{
color: white; color: white;
} }
.positive-button:hover{ .positive-button:hover:not([disabled]){
color: var(--focus0-alt1); color: var(--focus0-alt1);
background-color: var(--focus0-alt0); background-color: var(--focus0-alt0);
} }
.positive-button:active{ .positive-button:active:not([disabled]){
color: var(--focus0-alt0); color: var(--focus0-alt0);
background-color: var(--focus0-alt1); background-color: var(--focus0-alt1);
} }

View file

@ -64,6 +64,9 @@ class emotePanel extends panelObj{
this.setupInput(); this.setupInput();
this.renderEmoteLists(); this.renderEmoteLists();
//Call derived method
super.docSwitch();
} }
/** /**

View file

@ -27,6 +27,11 @@ class pmPanel extends panelObj{
constructor(client, panelDocument){ constructor(client, panelDocument){
super(client, "Private Messaging", "/panel/pm", panelDocument); super(client, "Private Messaging", "/panel/pm", panelDocument);
/**
* String to hold name of currently active sesh
*/
this.activeSesh = "";
this.defineListeners(); this.defineListeners();
} }
@ -34,33 +39,112 @@ class pmPanel extends panelObj{
} }
docSwitch(){ docSwitch(){
this.startSeshButton = this.panelDocument.querySelector('#pm-panel-start-sesh');
this.seshList = this.panelDocument.querySelector('#pm-panel-sesh-list'); 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');
this.setupInput(); this.setupInput();
this.renderSeshList(); this.renderSeshList();
//If we have an active sesh
if(this.activeSesh != null && this.activeSesh != ""){
//Render messages
this.renderMessages();
}
//Call derived method
super.docSwitch();
} }
/** /**
* Defines network related event listeners * Defines network related event listeners
*/ */
defineListeners(){ defineListeners(){
this.client.pmSocket.on("message", this.handlePM.bind(this));
this.client.pmSocket.on("sent", this.handlePM.bind(this));
} }
/** /**
* Defines input-related event handlers * Defines input-related event handlers
*/ */
setupInput(){ 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));
}
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{
//pull current session entry if it exists
const curEntry = this.panelDocument.querySelector(`[data-id="${nameObj.name}"]`);
//If it doesn't exist
if(curEntry == null){
//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);
//Send message out to server
this.client.pmSocket.emit("pm", {
recipients: sesh.recipients,
msg: this.seshPrompt.value
});
//Clear our prompt
this.seshPrompt.value = "";
}
} }
/** /**
* Render out current sesh array to sesh list UI * Render out current sesh array to sesh list UI
*/ */
renderSeshList(){ 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;
}
//For each session tracked by the pmHandler //For each session tracked by the pmHandler
for(const sesh of this.client.pmHandler.seshList){ for(const seshEntry of seshList){
this.renderSeshListEntry(sesh); this.renderSeshListEntry(seshEntry[1]);
} }
} }
@ -72,17 +156,171 @@ class pmPanel extends panelObj{
const entryDiv = document.createElement('div'); const entryDiv = document.createElement('div');
//Set conatiner div classes //Set conatiner div classes
entryDiv.classList.add('pm-panel-sesh-list-entry','interactive'); 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');
}
//Create sesh label //Create sesh label
const seshLabel = document.createElement('p'); const seshLabel = document.createElement('p');
//Create human-readable label out of members array //Create human-readable label out of members array
seshLabel.textContent = utils.unescapeEntities(sesh.recipients.sort().join(', ')); seshLabel.textContent = utils.unescapeEntities(sesh.id);
//append sesh label to entry div //append sesh label to entry div
entryDiv.appendChild(seshLabel); entryDiv.appendChild(seshLabel);
//Append entry div to sesh list //Append entry div to sesh list
this.seshList.appendChild(entryDiv); 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');
//Re-render message buffer
this.renderMessages();
}
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);
}
}
}
renderMessage(message){
const msgSpan = document.createElement('span');
const msgSender = document.createElement('p');
msgSender.innerText = utils.unescapeEntities(`${message.sender}:`);
msgSender.classList.add('pm-panel-sesh-message-sender');
const msgContent = document.createElement('p');
msgContent.innerText = utils.unescapeEntities(message.msg);
msgContent.classList.add('pm-panel-sesh-message-content');
msgSpan.appendChild(msgSender);
msgSpan.appendChild(msgContent);
this.seshBuffer.appendChild(msgSpan);
}
}
/**
* 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"){
/*
//Cook a new sesh from
const newSesh = new pmSesh({
//Split usernames by space
sender: this.client.user.user,
recipients: this.usernamePrompt.value.split(" ")
});
//Pop new sesh into pmHandler
this.client.pmHandler.seshList.set(newSesh.id, newSesh);
*/
//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();
}
} }
} }

View file

@ -1542,7 +1542,7 @@ class reschedulePopup extends schedulePopup{
this.media = media; this.media = media;
} }
schedule(event){ startSesh(event){
//If we clicked or hit enter //If we clicked or hit enter
if(event.key == null || event.key == "Enter"){ if(event.key == null || event.key == "Enter"){
//Get localized input date //Get localized input date

View file

@ -64,6 +64,9 @@ class settingsPanel extends panelObj{
this.renderSettings(); this.renderSettings();
this.setupInput(); this.setupInput();
//Call derived method
super.docSwitch();
} }
/** /**

View file

@ -36,7 +36,7 @@ class pmHandler{
/** /**
* List of PM Sessions * List of PM Sessions
*/ */
this.seshList = []; this.seshList = new Map();
this.defineListeners(); this.defineListeners();
this.setupInput(); this.setupInput();
@ -66,50 +66,63 @@ class pmHandler{
//Store whether or not current message has been consumed by an existing sesh //Store whether or not current message has been consumed by an existing sesh
let consumed = false; let consumed = false;
const nameObj = pmHandler.genSeshName(data);
//Create members array from scratch to avoid changing the input data for further processing //Create members array from scratch to avoid changing the input data for further processing
const members = []; const members = nameObj.recipients;
//Manually iterate through recipients
for(const member of data.recipients){
//check to make sure we're not adding ourselves
if(member != this.client.user.user){
//Copy relevant array members by value instead of reference
members.push(member);
}
}
//If this wasn't our message
if(data.sender != this.client.user.user){
//Push sender onto members list
members.push(data.sender);
}
//For each existing sesh //For each existing sesh
for(let seshIndex in this.seshList){ for(const seshEntry of this.seshList){
//Get current sesh //Pull sesh object from map entry
const sesh = this.seshList[seshIndex]; const sesh = seshEntry[1];
//Check to see if the length of sesh recipients equals current length (only check on arrays that actually make sense to save time) //If currently checked sesh ID matches calculated message sesh id
if(sesh.recipients.length == members.length){ if(sesh.id == nameObj.name){
/*Feels like cheating to have the JS engine to the hard bits by just telling it to sort them. //Dump collected message into the matching session
That being said, since the function is implemented into the JS Engine itself sesh.messages.push(data);
It will be quicker than any custom comparison code we can write*/
//Sort recipient lists so lists with the same user will be equal when joined together in a string and compare, if they're the same... //Add sesh to sesh map
if(sesh.recipients.sort().join() == members.sort().join()){ this.seshList.set(sesh.id, sesh);
//Dump collected message into the matching session
this.seshList[seshIndex].messages.push(data);
//Let the rest of the method know that we've consumed this message //Let the rest of the method know that we've consumed this message
consumed = true; consumed = true;
}
} }
} }
//If we made it through the loop without consuming the message //If we made it through the loop without consuming the message
if(!consumed){ if(!consumed){
//Add it to it's own fresh new sesh //Generate a new sesh
this.seshList.push(new pmSesh(data, client)); const sesh = new pmSesh(data, client);
//Add it to the sesh list
this.seshList.set(sesh.id, sesh);
}
}
static genSeshName(message){
const recipients = [];
//Manually iterate through recipients
for(const member of message.recipients){
//check to make sure we're not adding ourselves
if(member != client.user.user){
//Copy relevant array members by value instead of reference
recipients.push(member);
}
}
//If this wasn't our message
if(message.sender != client.user.user){
//Push sender onto members list
recipients.push(message.sender);
}
//Sort recipients
recipients.sort();
return {
name: recipients.join(', '),
recipients
} }
} }
} }
@ -128,29 +141,21 @@ class pmSesh{
*/ */
this.client = client; this.client = client;
const nameObj = pmHandler.genSeshName(message);
/** /**
* Members of session excluding the currently logged in user * Members of session excluding the currently logged in user
*/ */
this.recipients = []; this.recipients = nameObj.recipients
//Manually iterate through recipients /**
for(const member of message.recipients){ * Name of the chat sesh, named after out-going recipients
//check to make sure we're not adding ourselves */
if(member != this.client.user.user){ this.id = nameObj.name;
//Copy relevant array members by value instead of reference
this.recipients.push(member);
}
}
//If this wasn't our message
if(message.sender != this.client.user.user){
//Push sender onto members list
this.recipients.push(message.sender);
}
/** /**
* Array containing all session messages * Array containing all session messages
*/ */
this.messages = [message]; this.messages = (message.msg == "" || message.msg == null) ? [] : [message];
} }
} }

View file

@ -0,0 +1,23 @@
<!-- 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/>. -->
<link rel="stylesheet" type="text/css" href="/css/popup/startChatSesh.css">
<h3 class="popup-title">Start Chat Sesh</h3>
<div id="pm-sesh-popup-div">
<p>Enter user(s) to chat with:</p>
<input id="pm-sesh-popup-prompt" placeholder="Username(s)">
<button class="positive-button" id="pm-sesh-popup-button">Start Sesh</button>
</div>
<sup id="pm-sesh-popup-sup">Users must be online and connected to a channel (it doesn't have to be the same one.)</sup>