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
*/
sanatizeMessage(msg){
//if msg is empty or null
if(msg == null || msg == ''){
//Pimp slap that shit into fucking oblivion
return null;
}
//Normally I'd kill empty messages here
//But instead we're allowing them for sesh startups
//Trim and Sanatize for XSS
msg = validator.trim(validator.escape(msg));
//Return whether or not the shit was long enough
if(validator.isLength(msg, {min: 1, max: 255})){
//Return whether or not the shit was too long
if(validator.isLength(msg, {min: 0, max: 255})){
//If it's valid return the message
return msg;
}

View file

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

View file

@ -61,8 +61,32 @@ div.pm-panel-sesh-list-entry{
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{
margin: 0;
text-wrap: nowrap;
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;
}
button:hover{
button:hover:not([disabled]){
color: var(--focus0-alt1);
background-color: var(--focus0-alt0);
box-shadow: var(--focus-glow0);
}
button:active{
button:active:not([disabled]){
color: var(--focus0-alt0);
background-color: var(--focus0-alt1);
box-shadow: var(--focus-glow0-alt0);
@ -179,13 +179,13 @@ textarea{
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);
color: var(--danger0-alt0);
box-shadow: var(--danger-glow0);
}
.critical-danger-button:hover{
.critical-danger-button:hover:not([disabled]){
background-color: var(--danger0-alt2);
}
@ -219,12 +219,12 @@ textarea{
color: white;
}
.positive-button:hover{
.positive-button:hover:not([disabled]){
color: var(--focus0-alt1);
background-color: var(--focus0-alt0);
}
.positive-button:active{
.positive-button:active:not([disabled]){
color: var(--focus0-alt0);
background-color: var(--focus0-alt1);
}

View file

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

View file

@ -27,6 +27,11 @@ class pmPanel extends panelObj{
constructor(client, panelDocument){
super(client, "Private Messaging", "/panel/pm", panelDocument);
/**
* String to hold name of currently active sesh
*/
this.activeSesh = "";
this.defineListeners();
}
@ -34,33 +39,112 @@ class pmPanel extends panelObj{
}
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');
this.setupInput();
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
*/
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));
}
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
*/
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(const sesh of this.client.pmHandler.seshList){
this.renderSeshListEntry(sesh);
for(const seshEntry of seshList){
this.renderSeshListEntry(seshEntry[1]);
}
}
@ -72,17 +156,171 @@ class pmPanel extends panelObj{
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');
}
//Create sesh label
const seshLabel = document.createElement('p');
//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
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');
//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;
}
schedule(event){
startSesh(event){
//If we clicked or hit enter
if(event.key == null || event.key == "Enter"){
//Get localized input date

View file

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

View file

@ -36,7 +36,7 @@ class pmHandler{
/**
* List of PM Sessions
*/
this.seshList = [];
this.seshList = new Map();
this.defineListeners();
this.setupInput();
@ -66,50 +66,63 @@ class pmHandler{
//Store whether or not current message has been consumed by an existing sesh
let consumed = false;
const nameObj = pmHandler.genSeshName(data);
//Create members array from scratch to avoid changing the input data for further processing
const members = [];
//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);
}
const members = nameObj.recipients;
//For each existing sesh
for(let seshIndex in this.seshList){
//Get current sesh
const sesh = this.seshList[seshIndex];
for(const seshEntry of this.seshList){
//Pull sesh object from map entry
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(sesh.recipients.length == members.length){
/*Feels like cheating to have the JS engine to the hard bits by just telling it to sort them.
That being said, since the function is implemented into the JS Engine itself
It will be quicker than any custom comparison code we can write*/
//If currently checked sesh ID matches calculated message sesh id
if(sesh.id == nameObj.name){
//Dump collected message into the matching session
sesh.messages.push(data);
//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...
if(sesh.recipients.sort().join() == members.sort().join()){
//Dump collected message into the matching session
this.seshList[seshIndex].messages.push(data);
//Add sesh to sesh map
this.seshList.set(sesh.id, sesh);
//Let the rest of the method know that we've consumed this message
consumed = true;
}
//Let the rest of the method know that we've consumed this message
consumed = true;
}
}
//If we made it through the loop without consuming the message
if(!consumed){
//Add it to it's own fresh new sesh
this.seshList.push(new pmSesh(data, client));
//Generate a new sesh
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;
const nameObj = pmHandler.genSeshName(message);
/**
* 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){
//check to make sure we're not adding ourselves
if(member != this.client.user.user){
//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);
}
/**
* Name of the chat sesh, named after out-going recipients
*/
this.id = nameObj.name;
/**
* 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>