From 9a8def18d7ba55304d79879a86fc7b31e67feb98 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Tue, 7 Jan 2025 01:42:54 -0500 Subject: [PATCH] Started work on userlist profile tooltips and context menus. --- src/app/channel/tokebot.js | 4 +- src/controllers/panel/profileController.js | 42 ++++++++++ src/controllers/tooltip/profileController.js | 42 ++++++++++ src/routers/panelRouter.js | 4 + src/routers/tooltipRouter.js | 11 ++- src/views/partial/panels/profile.ejs | 32 ++++++++ src/views/partial/tooltip/altList.ejs | 3 + src/views/partial/tooltip/profile.ejs | 27 +++++++ www/css/channel.css | 2 + www/css/global.css | 17 ++++ www/css/panel/profile.css | 47 +++++++++++ www/css/tooltip/profile.css | 38 +++++++++ www/js/adminPanel.js | 15 +--- www/js/channel/chat.js | 4 + www/js/channel/userlist.js | 21 ++++- www/js/utils.js | 85 +++++++++++++++++++- 16 files changed, 372 insertions(+), 22 deletions(-) create mode 100644 src/controllers/panel/profileController.js create mode 100644 src/controllers/tooltip/profileController.js create mode 100644 src/views/partial/panels/profile.ejs create mode 100644 src/views/partial/tooltip/profile.ejs create mode 100644 www/css/panel/profile.css create mode 100644 www/css/tooltip/profile.css 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 . %> + +
+ <% if(profile == null){ %> +

Profile not found!

+ <% }else{ %> + View Full Profile +

<%- profile.user %>

+ +

Toke Count: <%- profile.tokeCount %>

+ <% if(profile.pronouns != '' && profile.pronouns != null){ %> +

Pronouns: <%- profile.pronouns %>

+ <% } %> +

Signature: <%- profile.signature %>

+

Bio:

+

<%- profile.bio %>

+ <% } %> +
\ 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 . %>

User: <%- alts[alt].user %>

ID: <%- alts[alt].id %>

+ <% if(alts[alt].pronouns != '' && alts[alt].pronouns != null){ %> +

Pronouns: <%- alts[alt].pronouns %>

+ <% } %>

Signature: <%- alts[alt].signature %>

Toke Count: <%- alts[alt].tokeCount %>

Joined: <%- alts[alt].date.toLocaleString() %>

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){ %> +

Profile not found!

+<% }else{ %> +

<%- profile.user %>

+ +

Toke Count: <%- profile.tokeCount %>

+ <% if(profile.pronouns != '' && profile.pronouns != null){ %> +

Pronouns: <%- profile.pronouns %>

+ <% } %> +

Signature: <%- profile.signature %>

+<% } %> \ 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