diff --git a/src/app/channel/tokebot.js b/src/app/channel/tokebot.js
index 90166e1..421605e 100644
--- a/src/app/channel/tokebot.js
+++ b/src/app/channel/tokebot.js
@@ -104,8 +104,8 @@ module.exports = class tokebot{
this.chatHandler.relayTokeWhisper(commandObj.socket, `Please wait ${this.cooldownCounter} seconds before starting a new group toke.`);
}
- //Toke command found, don't send as chat
- return false;
+ //Toke command found, and there isn't any extra text, don't send as chat (re-create fore.st tokebot behaviour)
+ return (commandObj.command != `!${commandObj.argumentArray[0]}`)
}else{
//No toke found, send it down the line, because shaming the user is funny
return true;
diff --git a/src/controllers/panel/profileController.js b/src/controllers/panel/profileController.js
new file mode 100644
index 0000000..ac58d44
--- /dev/null
+++ b/src/controllers/panel/profileController.js
@@ -0,0 +1,42 @@
+/*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 .*/
+
+//NPM Imports
+const {validationResult, matchedData} = require('express-validator');
+
+//local imports
+const {userModel} = require('../../schemas/user/userSchema');
+const {exceptionHandler, errorHandler} = require('../../utils/loggerUtils');
+
+//root index functions
+module.exports.get = async function(req, res){
+ try{
+ const validResult = validationResult(req);
+
+ if(validResult.isEmpty()){
+ const data = matchedData(req);
+ const profile = await userModel.findProfile({user: data.user});
+
+ return res.render('partial/panels/profile', {profile});
+ }else{
+ res.status(400);
+ return res.send({errors: validResult.array()})
+ }
+
+ }catch(err){
+ return exceptionHandler(res, err);
+ }
+}
\ No newline at end of file
diff --git a/src/controllers/tooltip/profileController.js b/src/controllers/tooltip/profileController.js
new file mode 100644
index 0000000..18f9cff
--- /dev/null
+++ b/src/controllers/tooltip/profileController.js
@@ -0,0 +1,42 @@
+/*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 .*/
+
+//NPM Imports
+const {validationResult, matchedData} = require('express-validator');
+
+//local imports
+const {userModel} = require('../../schemas/user/userSchema');
+const {exceptionHandler, errorHandler} = require('../../utils/loggerUtils');
+
+//root index functions
+module.exports.get = async function(req, res){
+ try{
+ const validResult = validationResult(req);
+
+ if(validResult.isEmpty()){
+ const data = matchedData(req);
+ const profile = await userModel.findProfile({user: data.user});
+
+ return res.render('partial/tooltip/profile', {profile});
+ }else{
+ res.status(400);
+ return res.send({errors: validResult.array()})
+ }
+
+ }catch(err){
+ return exceptionHandler(res, err);
+ }
+}
\ No newline at end of file
diff --git a/src/routers/panelRouter.js b/src/routers/panelRouter.js
index 1e6b5e9..908cdbd 100644
--- a/src/routers/panelRouter.js
+++ b/src/routers/panelRouter.js
@@ -22,6 +22,9 @@ const { Router } = require('express');
const placeholderController = require("../controllers/panel/placeholderController");
const emoteController = require("../controllers/panel/emoteController");
const popoutContainerController = require("../controllers/panel/popoutContainerController");
+const profileController = require("../controllers/panel/profileController");
+//Validators
+const accountValidator = require("../validators/accountValidator");
//globals
const router = Router();
@@ -30,5 +33,6 @@ const router = Router();
router.get('/placeholder', placeholderController.get);
router.get('/emote', emoteController.get);
router.get('/popoutContainer', popoutContainerController.get);
+router.get('/profile', accountValidator.user(), profileController.get);
module.exports = router;
diff --git a/src/routers/tooltipRouter.js b/src/routers/tooltipRouter.js
index 469c679..e1e925e 100644
--- a/src/routers/tooltipRouter.js
+++ b/src/routers/tooltipRouter.js
@@ -19,14 +19,19 @@ const { Router } = require('express');
//local imports
-const altListController = require("../controllers/tooltip/altListController");
-const permissionSchema = require("../schemas/permissionSchema");
+//DB Models
+const permissionModel = require("../schemas/permissionSchema");
+//Validators
const accountValidator = require("../validators/accountValidator");
+//controllers
+const altListController = require("../controllers/tooltip/altListController");
+const profileController = require("../controllers/tooltip/profileController");
//globals
const router = Router();
//routing functions
-router.get('/altList', accountValidator.user(), permissionSchema.reqPermCheck("adminPanel"), altListController.get);
+router.get('/altList', accountValidator.user(), permissionModel.reqPermCheck("adminPanel"), altListController.get);
+router.get('/profile', accountValidator.user(), profileController.get);
module.exports = router;
diff --git a/src/views/partial/panels/profile.ejs b/src/views/partial/panels/profile.ejs
new file mode 100644
index 0000000..c2ca2db
--- /dev/null
+++ b/src/views/partial/panels/profile.ejs
@@ -0,0 +1,32 @@
+<%# 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 . %>
+
+
\ No newline at end of file
diff --git a/src/views/partial/tooltip/altList.ejs b/src/views/partial/tooltip/altList.ejs
index baffda4..0ef19b8 100644
--- a/src/views/partial/tooltip/altList.ejs
+++ b/src/views/partial/tooltip/altList.ejs
@@ -19,6 +19,9 @@ along with this program. If not, see . %>
diff --git a/src/views/partial/tooltip/profile.ejs b/src/views/partial/tooltip/profile.ejs
new file mode 100644
index 0000000..dba9bb3
--- /dev/null
+++ b/src/views/partial/tooltip/profile.ejs
@@ -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 . %>
+
+<% if(profile == null){ %>
+
+<% } %>
\ No newline at end of file
diff --git a/www/css/channel.css b/www/css/channel.css
index 1a28cd7..e258c42 100644
--- a/www/css/channel.css
+++ b/www/css/channel.css
@@ -187,6 +187,7 @@ p.panel-head-element{
span.user-entry{
display: flex;
margin-bottom: 0.1em;
+ cursor: pointer;
}
.user-entry{
@@ -238,6 +239,7 @@ span.user-entry{
left: 0;
overflow-y: auto;
scrollbar-width: thin;
+ width: 75%;
}
#cpanel-pinned-div{
diff --git a/www/css/global.css b/www/css/global.css
index b1d666c..a93d0b6 100644
--- a/www/css/global.css
+++ b/www/css/global.css
@@ -161,8 +161,25 @@ div.tooltip{
min-width: 1em;
min-height: 1em;
padding: 0.5em;
+ z-index: 20;
}
p.tooltip, h3.tooltip{
margin: 0 auto;
+}
+
+/* context menu */
+.context-menu{
+ display: flex;
+ flex-direction: column;
+ align-items: stretch;
+}
+
+.context-menu h2{
+ margin: 0;
+ text-align: center;
+}
+
+.context-menu button{
+ margin: 0.05em 0;
}
\ No newline at end of file
diff --git a/www/css/panel/profile.css b/www/css/panel/profile.css
new file mode 100644
index 0000000..299a2de
--- /dev/null
+++ b/www/css/panel/profile.css
@@ -0,0 +1,47 @@
+/*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 .*/
+#profile-panel{
+ display: flex;
+ flex-direction: column;
+}
+
+.panel.profile-link{
+ text-align: right;
+}
+
+.panel.profile-name{
+ text-align: center;
+}
+
+.panel.profile-img{
+ margin: 0 2em;
+ max-height: 20em;
+ object-fit: contain;
+}
+
+p.panel{
+ margin: 0.2em 2em;
+ text-wrap: nowrap;
+}
+
+.panel.profile-bio-label{
+ margin-bottom: 0;
+}
+
+.panel.profile-bio{
+ margin-top: 0;
+ text-wrap: wrap;
+}
\ No newline at end of file
diff --git a/www/css/tooltip/profile.css b/www/css/tooltip/profile.css
new file mode 100644
index 0000000..d5f89d2
--- /dev/null
+++ b/www/css/tooltip/profile.css
@@ -0,0 +1,38 @@
+/*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 .*/
+div.tooltip{
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+}
+
+.tooltip .profile-img{
+ max-width: 6em;
+ max-height: 6em;
+ object-fit: contain;
+}
+
+.tooltip .profile-name{
+ text-align: center;
+ margin: 0;
+ text-wrap: nowrap;
+}
+
+.tooltip p{
+ text-align: left;
+ width: 100%;
+ text-wrap: nowrap;
+}
\ No newline at end of file
diff --git a/www/js/adminPanel.js b/www/js/adminPanel.js
index 298f767..f884168 100644
--- a/www/js/adminPanel.js
+++ b/www/js/adminPanel.js
@@ -307,20 +307,7 @@ class adminUserList{
//Splice username out of class name
const name = userName.id.replace('admin-user-list-name-','');
- //When the mouse starts to hover
- userName.addEventListener('mouseenter',(event)=>{
- //Create the tooltip
- const tooltip = new canopyUXUtils.tooltip(`altList?user=${name}`, true);
-
- //Do intial mouse move
- tooltip.moveToMouse(event);
-
- //Move the tooltip with the mouse
- userName.addEventListener('mousemove', tooltip.moveToMouse.bind(tooltip));
-
- //remove the tooltip on mouseleave
- userName.addEventListener('mouseleave', tooltip.remove.bind(tooltip));
- });
+ userName.addEventListener('mouseenter',(event)=>{utils.ux.displayTooltip(event, `altList?user=${name}`, true);});
}
for(let rankSelector of this.rankSelectors){
diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js
index b8c1eac..23a9a98 100644
--- a/www/js/channel/chat.js
+++ b/www/js/channel/chat.js
@@ -140,6 +140,10 @@ class chatBox{
this.displayAutocomplete();
}
+ tokeWith(user){
+ this.commandPreprocessor.preprocess(user == this.client.user.user ? "!toke up fuckers" : `!toke up ${user}`);
+ }
+
send(event){
if((!event || !event.key || event.key == "Enter") && this.chatPrompt.value){
this.commandPreprocessor.preprocess(this.chatPrompt.value);
diff --git a/www/js/channel/userlist.js b/www/js/channel/userlist.js
index 0dfd81a..8050ffc 100644
--- a/www/js/channel/userlist.js
+++ b/www/js/channel/userlist.js
@@ -101,7 +101,6 @@ class userList{
var highLevel = document.createElement('p');
highLevel.classList.add("user-list-high-level","high-level");
highLevel.innerHTML = `${user.highLevel}`;
- userSpan.appendChild(highLevel);
//Create nameplate
var userEntry = document.createElement('p');
@@ -115,10 +114,30 @@ class userList{
//Add classes to classList
userEntry.classList.add("chat-panel-users","user-entry",flair);
+ //Add high-level username to nameplate
+ userSpan.appendChild(highLevel);
userSpan.appendChild(userEntry);
+ //Setup profile tooltip
+ userSpan.addEventListener('mouseenter',(event)=>{utils.ux.displayTooltip(event, `profile?user=${user.user}`, true, null, true);});
+
+ //Setup profile context menu
+ userSpan.addEventListener('click', renderContextMenu.bind(this));
+ userSpan.addEventListener('contextmenu', renderContextMenu.bind(this));
this.userList.appendChild(userSpan);
+
+ function renderContextMenu(event){
+ //Setup menu map
+ let menuMap = new Map([
+ ["Profile", ()=>{this.client.cPanel.setActivePanel(new panelObj(client, `${user.user}`, `/panel/profile?user=${user.user}`))}],
+ ["Mention", ()=>{client.chatBox.catChat(`${user.user} `)}],
+ ["Toke With", ()=>{client.chatBox.tokeWith(user.user)}],
+ ]);
+
+ //Display the menu
+ utils.ux.displayContextMenu(event, user.user, menuMap);
+ }
}
toggleUI(show = !this.userDiv.checkVisibility()){
diff --git a/www/js/utils.js b/www/js/utils.js
index b723215..ea60e2c 100644
--- a/www/js/utils.js
+++ b/www/js/utils.js
@@ -34,13 +34,51 @@ class canopyUXUtils{
new canopyUXUtils.popup(`
Server Error:
${err.msg}
`);
});
}catch(err){
- console.error("Display Error Error:");
+ console.error("Display Error Body:");
console.error(body);
console.error("Display Error Error:");
console.error(err);
}
}
+ displayTooltip(event, content, ajaxTooltip, cb, soft = false){
+ //Create the tooltip
+ const tooltip = new canopyUXUtils.tooltip(content, ajaxTooltip, ()=>{
+ //Call mouse move again after ajax load to re-calculate position within context of the new content
+ tooltip.moveToMouse(event);
+ //If we have a callback function
+ if(typeof cb == "function"){
+ //Call async callback
+ cb();
+ }
+ });
+
+ //Move the tooltip with the mouse
+ event.target.addEventListener('mousemove', tooltip.moveToMouse.bind(tooltip));
+
+ //Do intial mouse move
+ tooltip.moveToMouse(event);
+
+ //remove the tooltip on mouseleave
+ event.target.addEventListener('mouseleave', tooltip.remove.bind(tooltip));
+
+ if(soft){
+ //remove the tooltip on context menu open
+ event.target.addEventListener('click', tooltip.remove.bind(tooltip));
+ event.target.addEventListener('contextmenu', tooltip.remove.bind(tooltip));
+ }
+ }
+
+ displayContextMenu(event, title, menuMap){
+ event.preventDefault();
+
+ //Create context menu
+ const contextMenu = new canopyUXUtils.contextMenu(title, menuMap);
+
+ //Move context menu to mouse
+ contextMenu.moveToMouse(event);
+ }
+
//We should really move this over to uxutils along with newrow & newtable functions
newTableCell(content, firstCol = false){
@@ -174,6 +212,50 @@ class canopyUXUtils{
}
}
+ static contextMenu = class extends this.tooltip{
+ constructor(title, menuMap){
+ //Call inherited tooltip constructor
+ super('Loading Menu...');
+ //Set tooltip class
+ this.tooltip.classList.add('context-menu');
+
+ //Set title and menu map
+ this.title = title;
+ this.menuMap = menuMap;
+
+ this.constructMenu();
+ }
+
+ constructMenu(){
+ //Clear out tooltip
+ this.tooltip.innerHTML = '';
+
+ //Create menu title
+ const menuTitle = document.createElement('h2');
+ menuTitle.innerHTML = this.title;
+
+ //Append the title to the tooltip
+ this.tooltip.append(menuTitle);
+
+ for(let choice of this.menuMap){
+ //Create button
+ const button = document.createElement('button');
+ button.innerHTML = choice[0];
+
+ //Add event listeners
+ button.addEventListener('click', choice[1]);
+
+ //Append the button to the menu div
+ this.tooltip.appendChild(button);
+ }
+
+ //Create event listener to remove tooltip whenever anything is clicked, inside or out of the menu
+ //Little hacky but we have to do it a bit later to prevent the opening event from closing the menu
+ setTimeout(()=>{document.body.addEventListener('click', this.remove.bind(this));}, 1);
+ setTimeout(()=>{document.body.addEventListener('contextmenu', this.remove.bind(this));}, 1);
+ }
+ }
+
static popup = class{
constructor(content, ajaxPopup = false, cb){
//Define non-popup node values
@@ -326,7 +408,6 @@ class canopyUXUtils{
}
drag(event){
-
if(this.dragLock){
if(this.leftHandle){
//get difference between mouse and right edge of element