From a4a1f6a65b19e30274408607db0b5bed8c8c90e1 Mon Sep 17 00:00:00 2001 From: rainbow napkin Date: Sun, 22 Dec 2024 13:46:08 -0500 Subject: [PATCH] Started work on personal emotes. --- src/app/channel/activeChannel.js | 3 +- src/app/channel/chatHandler.js | 33 ++++++++++++++++ src/app/channel/commandPreprocessor.js | 1 + src/app/channel/connectedUser.js | 15 +++++++ src/schemas/channel/channelSchema.js | 7 +++- src/schemas/userSchema.js | 54 +++++++++++++++++++++++++- src/validators/emoteValidator.js | 28 ++++++++++++- src/views/channel.ejs | 2 +- www/css/channel.css | 1 - www/css/global.css | 5 +++ www/css/panel/emote.css | 19 ++++++++- www/css/theme/movie-night.css | 5 +++ www/js/channel/chatPostprocessor.js | 1 + www/js/channel/commandPreprocessor.js | 5 +++ www/js/channel/cpanel.js | 37 ++++++++++++++---- www/js/channel/panels/emotePanel.js | 50 ++++++++++++++++++++++-- 16 files changed, 248 insertions(+), 18 deletions(-) diff --git a/src/app/channel/activeChannel.js b/src/app/channel/activeChannel.js index cc26724..452f363 100644 --- a/src/app/channel/activeChannel.js +++ b/src/app/channel/activeChannel.js @@ -56,7 +56,8 @@ module.exports = class{ //await this.sendClientMetadata(userDB, socket); await userObj.sendClientMetadata(); await userObj.sendSiteEmotes(); - await userObj.sendChanEmotes(); + await userObj.sendChanEmotes(chanDB); + await userObj.sendPersonalEmotes(userDB); //Send out the userlist this.broadcastUserList(socket.chan); diff --git a/src/app/channel/chatHandler.js b/src/app/channel/chatHandler.js index 0940d39..22a8bc0 100644 --- a/src/app/channel/chatHandler.js +++ b/src/app/channel/chatHandler.js @@ -17,6 +17,8 @@ along with this program. If not, see .*/ //local imports const commandPreprocessor = require('./commandPreprocessor'); const loggerUtils = require('../../utils/loggerUtils'); +const linkUtils = require('../../utils/linkUtils'); +const emoteValidator = require('../../validators/emoteValidator'); const {userModel} = require('../../schemas/userSchema'); module.exports = class{ @@ -29,6 +31,7 @@ module.exports = class{ socket.on("chatMessage", (data) => {this.handleChat(socket, data)}); socket.on("setFlair", (data) => {this.setFlair(socket, data)}); socket.on("setHighLevel", (data) => {this.setHighLevel(socket, data)}); + socket.on("addPersonalEmote", (data) => {this.addPersonalEmote(socket, data)}); } handleChat(socket, data){ @@ -79,6 +82,36 @@ module.exports = class{ } } + async addPersonalEmote(socket, data){ + //Sanatize and Validate input + const name = emoteValidator.manualName(data.name); + const link = emoteValidator.manualLink(data.link); + + //If we received good input + if(link && name){ + //Generate marked link object + var emote = await linkUtils.markLink(link); + + //If the link we have is an image or video + if(emote.type == 'image' || emote.type == 'video'){ + //Get user document from DB + const userDB = await userModel.findOne({user: socket.user.user}) + + //if we have a user in the DB + if(userDB != null){ + //Convert marked link into emote object with 1 ez step for only $19.95 + emote.name = name; + + //add emote to user document emotes list + userDB.emotes.push(emote); + + //Save user doc + await userDB.save(); + } + } + } + } + relayUserChat(socket, msg, type, links){ const user = this.server.getSocketInfo(socket); this.relayChat(user.user, user.flair, user.highLevel, msg, type, socket.chan, links) diff --git a/src/app/channel/commandPreprocessor.js b/src/app/channel/commandPreprocessor.js index ba5f69a..762e048 100644 --- a/src/app/channel/commandPreprocessor.js +++ b/src/app/channel/commandPreprocessor.js @@ -46,6 +46,7 @@ module.exports = class commandPreprocessor{ //split the command this.splitCommand(); + //Process the command await this.processServerCommand(); diff --git a/src/app/channel/connectedUser.js b/src/app/channel/connectedUser.js index 53aaa12..fc7ff43 100644 --- a/src/app/channel/connectedUser.js +++ b/src/app/channel/connectedUser.js @@ -19,6 +19,7 @@ const channelModel = require('../../schemas/channel/channelSchema'); const permissionModel = require('../../schemas/permissionSchema'); const flairModel = require('../../schemas/flairSchema'); const emoteModel = require('../../schemas/emoteSchema'); +const { userModel } = require('../../schemas/userSchema'); module.exports = class{ constructor(userDB, chanRank, channel, socket){ @@ -111,6 +112,20 @@ module.exports = class{ this.emit('chanEmotes', emoteList); } + async sendPersonalEmotes(userDB){ + //if we wherent handed a channel document + if(userDB == null){ + //Pull it based on channel name + userDB = await userModel.findOne({user: this.user}); + } + + //Pull emotes from channel + const emoteList = userDB.getEmotes(); + + //Send it off to the user + this.emit('personalEmotes', emoteList); + } + updateFlair(flair){ this.flair = flair; diff --git a/src/schemas/channel/channelSchema.js b/src/schemas/channel/channelSchema.js index 798f3e1..b1ef65d 100644 --- a/src/schemas/channel/channelSchema.js +++ b/src/schemas/channel/channelSchema.js @@ -19,13 +19,17 @@ const {mongoose} = require('mongoose'); const {validationResult, matchedData} = require('express-validator'); //Local Imports +//Server const server = require('../../server'); +//DB Models const statModel = require('../statSchema'); const {userModel} = require('../userSchema'); const permissionModel = require('../permissionSchema'); const emoteModel = require('../emoteSchema'); +//DB Schemas const channelPermissionSchema = require('./channelPermissionSchema'); const channelBanSchema = require('./channelBanSchema'); +//Utils const { exceptionHandler, errorHandler } = require('../../utils/loggerUtils'); const channelSchema = new mongoose.Schema({ @@ -77,7 +81,7 @@ const channelSchema = new mongoose.Schema({ type: mongoose.SchemaTypes.String, required: true }], - //Not re-using the site-wide schema because post save should call different functions + //Not re-using the site-wide schema because post/pre save should call different functions emotes: [{ name:{ type: mongoose.SchemaTypes.String, @@ -178,6 +182,7 @@ channelSchema.pre('save', async function (next){ } } + //if emotes where modified if(this.isModified('emotes')){ //Get the active Channel object from the application side of the house const activeChannel = server.channelManager.activeChannels.get(this.name); diff --git a/src/schemas/userSchema.js b/src/schemas/userSchema.js index 5c8f834..e65a0ab 100644 --- a/src/schemas/userSchema.js +++ b/src/schemas/userSchema.js @@ -18,10 +18,14 @@ along with this program. If not, see .*/ const {mongoose} = require('mongoose'); //local imports +//server const server = require('../server'); +//DB Models const statModel = require('./statSchema'); const flairModel = require('./flairSchema'); const permissionModel = require('./permissionSchema'); +const emoteModel = require('./emoteSchema'); +//Utils const hashUtil = require('../utils/hashUtils'); @@ -40,7 +44,9 @@ const userSchema = new mongoose.Schema({ required: true }, email: { - type: mongoose.SchemaTypes.String + type: mongoose.SchemaTypes.String, + optional: true, + default: "" }, date: { type: mongoose.SchemaTypes.Date, @@ -92,7 +98,24 @@ const userSchema = new mongoose.Schema({ type: mongoose.SchemaTypes.ObjectID, default: null, ref: "flair" - } + }, + //Not re-using the site-wide schema because post/pre save should call different functions + emotes: [{ + name:{ + type: mongoose.SchemaTypes.String, + required: true + }, + link:{ + type: mongoose.SchemaTypes.String, + required: true + }, + type:{ + type: mongoose.SchemaTypes.String, + required: true, + enum: emoteModel.typeEnum, + default: emoteModel.typeEnum[0] + } + }] }); //This is one of those places where you really DON'T want to use an arrow function over an anonymous one! @@ -124,6 +147,15 @@ userSchema.pre('save', async function (next){ await this.killAllSessions("Your site-wide rank has changed. Sign-in required."); } + //if emotes where modified + if(this.isModified('emotes')){ + //Get the active Channel object from the application side of the house + server.channelManager.crawlConnections(this.user, (conn)=>{ + //Send out emotes to each one + conn.sendPersonalEmotes(this); + }); + } + //All is good, continue on saving. next(); }); @@ -347,6 +379,24 @@ userSchema.methods.getTokeCount = function(){ return tokeCount; } +userSchema.methods.getEmotes = function(){ + //Create an empty array to hold our emote list + const emoteList = []; + + //For each channel emote + this.emotes.forEach((emote) => { + //Push an object with select information from the emote to the emote list + emoteList.push({ + name: emote.name, + link: emote.link, + type: emote.type + }); + }); + + //return the emote list + return emoteList; +} + //note: if you gotta call this from a request authenticated by it's user, make sure to kill that session first! userSchema.methods.killAllSessions = async function(reason = "A full log-out from all devices was requested for your account."){ //get authenticated sessions diff --git a/src/validators/emoteValidator.js b/src/validators/emoteValidator.js index b7b9655..cdff9e7 100644 --- a/src/validators/emoteValidator.js +++ b/src/validators/emoteValidator.js @@ -16,8 +16,34 @@ along with this program. If not, see .*/ //NPM Imports const { check } = require('express-validator'); +const validator = require('validator');//We need validators for express-less code too! module.exports = { name: (field = 'name') => check(field).escape().trim().isAlphanumeric().isLength({min: 1, max: 14}), - link: (field = 'link') => check(field).trim().isURL() + link: (field = 'link') => check(field).trim().isURL(), + manualName: (input) => { + //Trim and sanatize input + const clean = validator.trim(validator.escape(input)); + + //if cleaned input is a proper emote name + if(validator.isLength(clean, {min: 1, max: 14}) && validator.isAlphanumeric(clean)){ + //return cleaned input + return clean; + } + + //otherwise return false + return false; + }, + manualLink: (input) => { + //Trim the input + const clean = validator.trim(input) + + //If we have a URL return the trimmed input + if(validator.isURL(clean)){ + return clean; + } + + //otherwise return false + return false; + } } \ No newline at end of file diff --git a/src/views/channel.ejs b/src/views/channel.ejs index b310b43..c2806d1 100644 --- a/src/views/channel.ejs +++ b/src/views/channel.ejs @@ -64,7 +64,7 @@ along with this program. If not, see .--> -

NULL Users

+

NULL Users

diff --git a/www/css/channel.css b/www/css/channel.css index ff5c10b..a782f99 100644 --- a/www/css/channel.css +++ b/www/css/channel.css @@ -216,7 +216,6 @@ span.user-entry{ #chat-panel-user-count{ white-space: nowrap; user-select: none; - cursor:pointer; } #media-panel-show-chat-icon{ diff --git a/www/css/global.css b/www/css/global.css index 5a65a85..3f1a715 100644 --- a/www/css/global.css +++ b/www/css/global.css @@ -69,6 +69,11 @@ div.dynamic-container{ overflow: auto; } +.interactive{ + cursor: pointer; + user-select: none; +} + /* Navbar */ #navbar{ display: flex; diff --git a/www/css/panel/emote.css b/www/css/panel/emote.css index cb0a565..38fba44 100644 --- a/www/css/panel/emote.css +++ b/www/css/panel/emote.css @@ -22,10 +22,10 @@ div.emote-panel-list-emote{ width: 9em; display: flex; + position: relative; flex-direction: column; padding: 0.5em 0; margin: 0.5em; - user-select: none; cursor: pointer; } @@ -43,6 +43,7 @@ p.emote-list-title{ max-height: 8em; max-width: 8em; margin: auto; + object-fit: contain; } .emote-list-big-media{ @@ -62,4 +63,20 @@ div.panel-control-prompt{ #new-emote-button{ margin-left: 0.3em; +} + +span.emote-list-trash-icon{ + position: absolute; + display: flex; + width: 1.5em; + height: 1.5em; + border-radius: 1em; + top: -0.5em; + right: -0.5em; +} + +i.emote-list-trash-icon{ + flex: 1; + text-align: center; + margin: auto; } \ No newline at end of file diff --git a/www/css/theme/movie-night.css b/www/css/theme/movie-night.css index c74f983..179ef67 100644 --- a/www/css/theme/movie-night.css +++ b/www/css/theme/movie-night.css @@ -365,4 +365,9 @@ div.emote-panel-list-emote{ text-shadow: var(--focus-glow0-alt0); border: 1px solid var(--focus0-alt1); box-shadow: var(--focus-glow0-alt0), var(--focus-glow0-alt0-inset); +} + +span.emote-list-trash-icon{ + background-color: var(--bg2); + border: 1px solid var(--accent0) } \ No newline at end of file diff --git a/www/js/channel/chatPostprocessor.js b/www/js/channel/chatPostprocessor.js index 36ae396..614a262 100644 --- a/www/js/channel/chatPostprocessor.js +++ b/www/js/channel/chatPostprocessor.js @@ -32,6 +32,7 @@ class chatPostprocessor{ //Inject the pre-processed chat into the chatEntry node this.injectBody(); + //Return the pre-processed node return this.chatEntry; } diff --git a/www/js/channel/commandPreprocessor.js b/www/js/channel/commandPreprocessor.js index 68c0091..9c3a4f4 100644 --- a/www/js/channel/commandPreprocessor.js +++ b/www/js/channel/commandPreprocessor.js @@ -16,6 +16,7 @@ class commandPreprocessor{ //When we receive site-wide emote list this.client.socket.on("siteEmotes", this.setSiteEmotes.bind(this)); this.client.socket.on("chanEmotes", this.setChanEmotes.bind(this)); + this.client.socket.on("personalEmotes", this.setPersonalEmotes.bind(this)); } preprocess(command){ @@ -104,6 +105,10 @@ class commandPreprocessor{ this.emotes.chan = data; } + setPersonalEmotes(data){ + this.emotes.personal = data; + } + getEmoteByLink(link){ //Create an empty variable to hold the found emote var foundEmote = null; diff --git a/www/js/channel/cpanel.js b/www/js/channel/cpanel.js index 1b90943..560be45 100644 --- a/www/js/channel/cpanel.js +++ b/www/js/channel/cpanel.js @@ -73,7 +73,11 @@ class cPanel{ this.activePanel.docSwitch(); } - hideActivePanel(){ + hideActivePanel(event, keepAlive = false){ + if(!keepAlive){ + this.activePanel.closer(); + } + //Hide the panel this.activePanelDiv.style.display = "none"; //Clear out the panel @@ -84,12 +88,12 @@ class cPanel{ pinPanel(){ this.setPinnedPanel(this.activePanel, this.activePanelDoc.innerHTML); - this.hideActivePanel(); + this.hideActivePanel(null, true); } popActivePanel(){ this.popPanel(this.activePanel, this.activePanelDoc.innerHTML); - this.hideActivePanel(); + this.hideActivePanel(null, true); } async setPinnedPanel(panel, panelBody){ @@ -113,19 +117,24 @@ class cPanel{ this.pinnedPanelDragger.fixCutoff(); } - hidePinnedPanel(){ + hidePinnedPanel(event, keepAlive = false){ this.pinnedPanelDiv.style.display = "none"; + + if(!keepAlive){ + this.pinnedPanel.closer(); + } + this.pinnedPanel = null; } unpinPanel(){ this.setActivePanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML); - this.hidePinnedPanel(); + this.hidePinnedPanel(null, true); } popPinnedPanel(){ this.popPanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML); - this.hidePinnedPanel(); + this.hidePinnedPanel(null, true); } popPanel(panel, panelBody){ @@ -154,6 +163,10 @@ class panelObj{ docSwitch(){ } + + closer(){ + console.log('closer'); + } } class poppedPanel{ @@ -174,6 +187,8 @@ class poppedPanel{ //Functions this.cPanel = cPanel; + this.keepAlive = false; + //Continue constructor asynchrnously this.asyncConstructor(); } @@ -221,13 +236,19 @@ class poppedPanel{ } closer(){ - this.cPanel.poppedPanels.splice(this.cPanel.poppedPanels.indexOf(this),1); + if(!this.keepAlive){ + this.panel.closer(); + } + + this.cPanel.poppedPanels.splice(this.cPanel.poppedPanels.indexOf(this),1); } unpop(){ //Set active panel this.cPanel.setActivePanel(this.panel, this.panelDoc.innerHTML); + this.keepAlive = true; + //Close the popped window this.window.close(); } @@ -235,6 +256,8 @@ class poppedPanel{ pin(){ this.cPanel.setPinnedPanel(this.panel, this.panelDoc.innerHTML); + this.keepAlive = true; + this.window.close(); } diff --git a/www/js/channel/panels/emotePanel.js b/www/js/channel/panels/emotePanel.js index efbc593..47551df 100644 --- a/www/js/channel/panels/emotePanel.js +++ b/www/js/channel/panels/emotePanel.js @@ -1,6 +1,13 @@ class emotePanel extends panelObj{ constructor(client, panelDocument){ super(client, "Emote Palette", "/panel/emote", panelDocument); + + this.client.socket.on("personalEmotes", this.renderEmoteLists.bind(this)); + } + + closer(){ + this.client.socket.off("personalEmotes", this.renderEmoteLists.bind(this)); + console.log('emote closer'); } docSwitch(){ @@ -19,6 +26,10 @@ class emotePanel extends panelObj{ this.searchPrompt = this.panelDocument.querySelector('#emote-panel-search-prompt'); + this.personalEmoteLinkPrompt = this.panelDocument.querySelector('#new-emote-link-input'); + this.personalEmoteNamePrompt = this.panelDocument.querySelector('#new-emote-name-input'); + this.personalEmoteAddButton = this.panelDocument.querySelector('#new-emote-button'); + this.setupInput(); this.renderEmoteLists(); @@ -46,6 +57,9 @@ class emotePanel extends panelObj{ this.searchPrompt.removeEventListener('keyup', this.renderEmoteLists.bind(this)); this.searchPrompt.addEventListener('keyup', this.renderEmoteLists.bind(this)); + + this.personalEmoteAddButton.removeEventListener("click", this.addPersonalEmote.bind(this)); + this.personalEmoteAddButton.addEventListener("click", this.addPersonalEmote.bind(this)); } toggleSiteEmotes(event){ @@ -72,7 +86,7 @@ class emotePanel extends panelObj{ useEmote(emote){ //If we're using this from the active panel - if(client.cPanel.activePanel == this){ + if(this.client.cPanel.activePanel == this){ //Close it this.client.cPanel.hideActivePanel(); } @@ -81,6 +95,19 @@ class emotePanel extends panelObj{ this.client.chatBox.chatPrompt.value += `[${emote}]`; } + addPersonalEmote(event){ + //Collect input + const name = this.personalEmoteNamePrompt.value; + const link = this.personalEmoteLinkPrompt.value; + + //Empty out prompts + this.personalEmoteNamePrompt.value = ''; + this.personalEmoteLinkPrompt.value = ''; + + //Send emote to server + this.client.socket.emit("addPersonalEmote", {name, link}); + } + renderEmoteLists(){ var search = this.searchPrompt.value; @@ -102,10 +129,10 @@ class emotePanel extends panelObj{ this.renderEmotes(siteEmotes, this.siteEmoteList); this.renderEmotes(chanEmotes, this.chanEmoteList); - this.renderEmotes(personalEmotes, this.personalEmoteList); + this.renderEmotes(personalEmotes, this.personalEmoteList, true); } - renderEmotes(emoteList, container){ + renderEmotes(emoteList, container, personal = false){ //Clear out the container container.innerHTML = ''; @@ -164,6 +191,23 @@ class emotePanel extends panelObj{ emoteTitle.classList.add('emote-list-title'); //Set emote title emoteTitle.innerHTML = `[${emote.name}]`; + + //if we're rendering personal emotes + if(personal){ + //create span to hold trash icon + const trashSpan = document.createElement('span'); + trashSpan.classList.add('emote-list-trash-icon'); + + //Create trash icon + const trashIcon = document.createElement('i'); + trashIcon.classList.add('emote-list-trash-icon', 'bi-trash-fill'); + + //Add trash icon to trash span + trashSpan.appendChild(trashIcon); + + //append trash span to emote div + emoteDiv.appendChild(trashSpan); + } //Add the emote media to the emote span emoteDiv.appendChild(emoteMedia);