diff --git a/src/app/pm/pmHandler.js b/src/app/pm/pmHandler.js
index cf8daab..0014ec2 100644
--- a/src/app/pm/pmHandler.js
+++ b/src/app/pm/pmHandler.js
@@ -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;
}
diff --git a/src/views/partial/panels/pm.ejs b/src/views/partial/panels/pm.ejs
index af3e372..958a18c 100644
--- a/src/views/partial/panels/pm.ejs
+++ b/src/views/partial/panels/pm.ejs
@@ -26,11 +26,13 @@ along with this program. If not, see . %>
-
+
+
Start a sesh to start chatting!
+
-
-
+
+
diff --git a/www/css/panel/pm.css b/www/css/panel/pm.css
index df2db42..2e6663c 100644
--- a/www/css/panel/pm.css
+++ b/www/css/panel/pm.css
@@ -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%;
}
\ No newline at end of file
diff --git a/www/css/popup/startChatSesh.css b/www/css/popup/startChatSesh.css
new file mode 100644
index 0000000..86cd92f
--- /dev/null
+++ b/www/css/popup/startChatSesh.css
@@ -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 .*/
+#pm-sesh-popup-div{
+ display: flex;
+}
+
+
+#pm-sesh-popup-div p{
+ margin: 0;
+}
+
+#pm-sesh-popup-sup{
+ font-size: 0.7em
+}
\ No newline at end of file
diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css
index 96e5793..00e2f24 100644
--- a/www/css/theme/movie-night.css
+++ b/www/css/theme/movie-night.css
@@ -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);
}
diff --git a/www/js/channel/panels/emotePanel.js b/www/js/channel/panels/emotePanel.js
index 877b4f7..ee19936 100644
--- a/www/js/channel/panels/emotePanel.js
+++ b/www/js/channel/panels/emotePanel.js
@@ -64,6 +64,9 @@ class emotePanel extends panelObj{
this.setupInput();
this.renderEmoteLists();
+
+ //Call derived method
+ super.docSwitch();
}
/**
diff --git a/www/js/channel/panels/pmPanel.js b/www/js/channel/panels/pmPanel.js
index e502f4f..85e191e 100644
--- a/www/js/channel/panels/pmPanel.js
+++ b/www/js/channel/panels/pmPanel.js
@@ -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();
+ }
}
}
\ No newline at end of file
diff --git a/www/js/channel/panels/queuePanel/queuePanel.js b/www/js/channel/panels/queuePanel/queuePanel.js
index 5fb159e..2b05766 100644
--- a/www/js/channel/panels/queuePanel/queuePanel.js
+++ b/www/js/channel/panels/queuePanel/queuePanel.js
@@ -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
diff --git a/www/js/channel/panels/settingsPanel.js b/www/js/channel/panels/settingsPanel.js
index 7f6b707..976afee 100644
--- a/www/js/channel/panels/settingsPanel.js
+++ b/www/js/channel/panels/settingsPanel.js
@@ -64,6 +64,9 @@ class settingsPanel extends panelObj{
this.renderSettings();
this.setupInput();
+
+ //Call derived method
+ super.docSwitch();
}
/**
diff --git a/www/js/channel/pmHandler.js b/www/js/channel/pmHandler.js
index 09e1a3a..c3562f5 100644
--- a/www/js/channel/pmHandler.js
+++ b/www/js/channel/pmHandler.js
@@ -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];
}
}
\ No newline at end of file
diff --git a/www/popup/startChatSesh.html b/www/popup/startChatSesh.html
new file mode 100644
index 0000000..b8bb3cc
--- /dev/null
+++ b/www/popup/startChatSesh.html
@@ -0,0 +1,23 @@
+
+
+
Start Chat Sesh
+
+
Enter user(s) to chat with:
+
+
+
+Users must be online and connected to a channel (it doesn't have to be the same one.)
\ No newline at end of file