Started work on personal emotes.

This commit is contained in:
rainbow napkin 2024-12-22 13:46:08 -05:00
parent db5fac83ab
commit a4a1f6a65b
16 changed files with 248 additions and 18 deletions

View file

@ -56,7 +56,8 @@ module.exports = class{
//await this.sendClientMetadata(userDB, socket); //await this.sendClientMetadata(userDB, socket);
await userObj.sendClientMetadata(); await userObj.sendClientMetadata();
await userObj.sendSiteEmotes(); await userObj.sendSiteEmotes();
await userObj.sendChanEmotes(); await userObj.sendChanEmotes(chanDB);
await userObj.sendPersonalEmotes(userDB);
//Send out the userlist //Send out the userlist
this.broadcastUserList(socket.chan); this.broadcastUserList(socket.chan);

View file

@ -17,6 +17,8 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//local imports //local imports
const commandPreprocessor = require('./commandPreprocessor'); const commandPreprocessor = require('./commandPreprocessor');
const loggerUtils = require('../../utils/loggerUtils'); const loggerUtils = require('../../utils/loggerUtils');
const linkUtils = require('../../utils/linkUtils');
const emoteValidator = require('../../validators/emoteValidator');
const {userModel} = require('../../schemas/userSchema'); const {userModel} = require('../../schemas/userSchema');
module.exports = class{ module.exports = class{
@ -29,6 +31,7 @@ module.exports = class{
socket.on("chatMessage", (data) => {this.handleChat(socket, data)}); socket.on("chatMessage", (data) => {this.handleChat(socket, data)});
socket.on("setFlair", (data) => {this.setFlair(socket, data)}); socket.on("setFlair", (data) => {this.setFlair(socket, data)});
socket.on("setHighLevel", (data) => {this.setHighLevel(socket, data)}); socket.on("setHighLevel", (data) => {this.setHighLevel(socket, data)});
socket.on("addPersonalEmote", (data) => {this.addPersonalEmote(socket, data)});
} }
handleChat(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){ relayUserChat(socket, msg, type, links){
const user = this.server.getSocketInfo(socket); const user = this.server.getSocketInfo(socket);
this.relayChat(user.user, user.flair, user.highLevel, msg, type, socket.chan, links) this.relayChat(user.user, user.flair, user.highLevel, msg, type, socket.chan, links)

View file

@ -46,6 +46,7 @@ module.exports = class commandPreprocessor{
//split the command //split the command
this.splitCommand(); this.splitCommand();
//Process the command //Process the command
await this.processServerCommand(); await this.processServerCommand();

View file

@ -19,6 +19,7 @@ const channelModel = require('../../schemas/channel/channelSchema');
const permissionModel = require('../../schemas/permissionSchema'); const permissionModel = require('../../schemas/permissionSchema');
const flairModel = require('../../schemas/flairSchema'); const flairModel = require('../../schemas/flairSchema');
const emoteModel = require('../../schemas/emoteSchema'); const emoteModel = require('../../schemas/emoteSchema');
const { userModel } = require('../../schemas/userSchema');
module.exports = class{ module.exports = class{
constructor(userDB, chanRank, channel, socket){ constructor(userDB, chanRank, channel, socket){
@ -111,6 +112,20 @@ module.exports = class{
this.emit('chanEmotes', emoteList); 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){ updateFlair(flair){
this.flair = flair; this.flair = flair;

View file

@ -19,13 +19,17 @@ const {mongoose} = require('mongoose');
const {validationResult, matchedData} = require('express-validator'); const {validationResult, matchedData} = require('express-validator');
//Local Imports //Local Imports
//Server
const server = require('../../server'); const server = require('../../server');
//DB Models
const statModel = require('../statSchema'); const statModel = require('../statSchema');
const {userModel} = require('../userSchema'); const {userModel} = require('../userSchema');
const permissionModel = require('../permissionSchema'); const permissionModel = require('../permissionSchema');
const emoteModel = require('../emoteSchema'); const emoteModel = require('../emoteSchema');
//DB Schemas
const channelPermissionSchema = require('./channelPermissionSchema'); const channelPermissionSchema = require('./channelPermissionSchema');
const channelBanSchema = require('./channelBanSchema'); const channelBanSchema = require('./channelBanSchema');
//Utils
const { exceptionHandler, errorHandler } = require('../../utils/loggerUtils'); const { exceptionHandler, errorHandler } = require('../../utils/loggerUtils');
const channelSchema = new mongoose.Schema({ const channelSchema = new mongoose.Schema({
@ -77,7 +81,7 @@ const channelSchema = new mongoose.Schema({
type: mongoose.SchemaTypes.String, type: mongoose.SchemaTypes.String,
required: true 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: [{ emotes: [{
name:{ name:{
type: mongoose.SchemaTypes.String, type: mongoose.SchemaTypes.String,
@ -178,6 +182,7 @@ channelSchema.pre('save', async function (next){
} }
} }
//if emotes where modified
if(this.isModified('emotes')){ if(this.isModified('emotes')){
//Get the active Channel object from the application side of the house //Get the active Channel object from the application side of the house
const activeChannel = server.channelManager.activeChannels.get(this.name); const activeChannel = server.channelManager.activeChannels.get(this.name);

View file

@ -18,10 +18,14 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
const {mongoose} = require('mongoose'); const {mongoose} = require('mongoose');
//local imports //local imports
//server
const server = require('../server'); const server = require('../server');
//DB Models
const statModel = require('./statSchema'); const statModel = require('./statSchema');
const flairModel = require('./flairSchema'); const flairModel = require('./flairSchema');
const permissionModel = require('./permissionSchema'); const permissionModel = require('./permissionSchema');
const emoteModel = require('./emoteSchema');
//Utils
const hashUtil = require('../utils/hashUtils'); const hashUtil = require('../utils/hashUtils');
@ -40,7 +44,9 @@ const userSchema = new mongoose.Schema({
required: true required: true
}, },
email: { email: {
type: mongoose.SchemaTypes.String type: mongoose.SchemaTypes.String,
optional: true,
default: ""
}, },
date: { date: {
type: mongoose.SchemaTypes.Date, type: mongoose.SchemaTypes.Date,
@ -92,7 +98,24 @@ const userSchema = new mongoose.Schema({
type: mongoose.SchemaTypes.ObjectID, type: mongoose.SchemaTypes.ObjectID,
default: null, default: null,
ref: "flair" 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! //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."); 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. //All is good, continue on saving.
next(); next();
}); });
@ -347,6 +379,24 @@ userSchema.methods.getTokeCount = function(){
return tokeCount; 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! //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."){ userSchema.methods.killAllSessions = async function(reason = "A full log-out from all devices was requested for your account."){
//get authenticated sessions //get authenticated sessions

View file

@ -16,8 +16,34 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.*/
//NPM Imports //NPM Imports
const { check } = require('express-validator'); const { check } = require('express-validator');
const validator = require('validator');//We need validators for express-less code too!
module.exports = { module.exports = {
name: (field = 'name') => check(field).escape().trim().isAlphanumeric().isLength({min: 1, max: 14}), 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;
}
} }

View file

@ -64,7 +64,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.-->
<option>Flair</option> <option>Flair</option>
</select> </select>
<span class="chat-panel panel-head-spacer-span" id="chat-panel-head-spacer-span"></span> <span class="chat-panel panel-head-spacer-span" id="chat-panel-head-spacer-span"></span>
<p class="chat-panel panel-head-element" id="chat-panel-user-count">NULL Users</p> <p class="chat-panel panel-head-element interactive" id="chat-panel-user-count">NULL Users</p>
<i class="chat-panel panel-head-element bi-caret-down-fill" id="chat-panel-users-toggle"></i> <i class="chat-panel panel-head-element bi-caret-down-fill" id="chat-panel-users-toggle"></i>
</div> </div>
<div class="chat-panel" id="chat-panel-main-div"> <div class="chat-panel" id="chat-panel-main-div">

View file

@ -216,7 +216,6 @@ span.user-entry{
#chat-panel-user-count{ #chat-panel-user-count{
white-space: nowrap; white-space: nowrap;
user-select: none; user-select: none;
cursor:pointer;
} }
#media-panel-show-chat-icon{ #media-panel-show-chat-icon{

View file

@ -69,6 +69,11 @@ div.dynamic-container{
overflow: auto; overflow: auto;
} }
.interactive{
cursor: pointer;
user-select: none;
}
/* Navbar */ /* Navbar */
#navbar{ #navbar{
display: flex; display: flex;

View file

@ -22,10 +22,10 @@
div.emote-panel-list-emote{ div.emote-panel-list-emote{
width: 9em; width: 9em;
display: flex; display: flex;
position: relative;
flex-direction: column; flex-direction: column;
padding: 0.5em 0; padding: 0.5em 0;
margin: 0.5em; margin: 0.5em;
user-select: none;
cursor: pointer; cursor: pointer;
} }
@ -43,6 +43,7 @@ p.emote-list-title{
max-height: 8em; max-height: 8em;
max-width: 8em; max-width: 8em;
margin: auto; margin: auto;
object-fit: contain;
} }
.emote-list-big-media{ .emote-list-big-media{
@ -63,3 +64,19 @@ div.panel-control-prompt{
#new-emote-button{ #new-emote-button{
margin-left: 0.3em; 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;
}

View file

@ -366,3 +366,8 @@ div.emote-panel-list-emote{
border: 1px solid var(--focus0-alt1); border: 1px solid var(--focus0-alt1);
box-shadow: var(--focus-glow0-alt0), var(--focus-glow0-alt0-inset); 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)
}

View file

@ -32,6 +32,7 @@ class chatPostprocessor{
//Inject the pre-processed chat into the chatEntry node //Inject the pre-processed chat into the chatEntry node
this.injectBody(); this.injectBody();
//Return the pre-processed node //Return the pre-processed node
return this.chatEntry; return this.chatEntry;
} }

View file

@ -16,6 +16,7 @@ class commandPreprocessor{
//When we receive site-wide emote list //When we receive site-wide emote list
this.client.socket.on("siteEmotes", this.setSiteEmotes.bind(this)); this.client.socket.on("siteEmotes", this.setSiteEmotes.bind(this));
this.client.socket.on("chanEmotes", this.setChanEmotes.bind(this)); this.client.socket.on("chanEmotes", this.setChanEmotes.bind(this));
this.client.socket.on("personalEmotes", this.setPersonalEmotes.bind(this));
} }
preprocess(command){ preprocess(command){
@ -104,6 +105,10 @@ class commandPreprocessor{
this.emotes.chan = data; this.emotes.chan = data;
} }
setPersonalEmotes(data){
this.emotes.personal = data;
}
getEmoteByLink(link){ getEmoteByLink(link){
//Create an empty variable to hold the found emote //Create an empty variable to hold the found emote
var foundEmote = null; var foundEmote = null;

View file

@ -73,7 +73,11 @@ class cPanel{
this.activePanel.docSwitch(); this.activePanel.docSwitch();
} }
hideActivePanel(){ hideActivePanel(event, keepAlive = false){
if(!keepAlive){
this.activePanel.closer();
}
//Hide the panel //Hide the panel
this.activePanelDiv.style.display = "none"; this.activePanelDiv.style.display = "none";
//Clear out the panel //Clear out the panel
@ -84,12 +88,12 @@ class cPanel{
pinPanel(){ pinPanel(){
this.setPinnedPanel(this.activePanel, this.activePanelDoc.innerHTML); this.setPinnedPanel(this.activePanel, this.activePanelDoc.innerHTML);
this.hideActivePanel(); this.hideActivePanel(null, true);
} }
popActivePanel(){ popActivePanel(){
this.popPanel(this.activePanel, this.activePanelDoc.innerHTML); this.popPanel(this.activePanel, this.activePanelDoc.innerHTML);
this.hideActivePanel(); this.hideActivePanel(null, true);
} }
async setPinnedPanel(panel, panelBody){ async setPinnedPanel(panel, panelBody){
@ -113,19 +117,24 @@ class cPanel{
this.pinnedPanelDragger.fixCutoff(); this.pinnedPanelDragger.fixCutoff();
} }
hidePinnedPanel(){ hidePinnedPanel(event, keepAlive = false){
this.pinnedPanelDiv.style.display = "none"; this.pinnedPanelDiv.style.display = "none";
if(!keepAlive){
this.pinnedPanel.closer();
}
this.pinnedPanel = null; this.pinnedPanel = null;
} }
unpinPanel(){ unpinPanel(){
this.setActivePanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML); this.setActivePanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML);
this.hidePinnedPanel(); this.hidePinnedPanel(null, true);
} }
popPinnedPanel(){ popPinnedPanel(){
this.popPanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML); this.popPanel(this.pinnedPanel, this.pinnedPanelDoc.innerHTML);
this.hidePinnedPanel(); this.hidePinnedPanel(null, true);
} }
popPanel(panel, panelBody){ popPanel(panel, panelBody){
@ -154,6 +163,10 @@ class panelObj{
docSwitch(){ docSwitch(){
} }
closer(){
console.log('closer');
}
} }
class poppedPanel{ class poppedPanel{
@ -174,6 +187,8 @@ class poppedPanel{
//Functions //Functions
this.cPanel = cPanel; this.cPanel = cPanel;
this.keepAlive = false;
//Continue constructor asynchrnously //Continue constructor asynchrnously
this.asyncConstructor(); this.asyncConstructor();
} }
@ -221,13 +236,19 @@ class poppedPanel{
} }
closer(){ 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(){ unpop(){
//Set active panel //Set active panel
this.cPanel.setActivePanel(this.panel, this.panelDoc.innerHTML); this.cPanel.setActivePanel(this.panel, this.panelDoc.innerHTML);
this.keepAlive = true;
//Close the popped window //Close the popped window
this.window.close(); this.window.close();
} }
@ -235,6 +256,8 @@ class poppedPanel{
pin(){ pin(){
this.cPanel.setPinnedPanel(this.panel, this.panelDoc.innerHTML); this.cPanel.setPinnedPanel(this.panel, this.panelDoc.innerHTML);
this.keepAlive = true;
this.window.close(); this.window.close();
} }

View file

@ -1,6 +1,13 @@
class emotePanel extends panelObj{ class emotePanel extends panelObj{
constructor(client, panelDocument){ constructor(client, panelDocument){
super(client, "Emote Palette", "/panel/emote", 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(){ docSwitch(){
@ -19,6 +26,10 @@ class emotePanel extends panelObj{
this.searchPrompt = this.panelDocument.querySelector('#emote-panel-search-prompt'); 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.setupInput();
this.renderEmoteLists(); this.renderEmoteLists();
@ -46,6 +57,9 @@ class emotePanel extends panelObj{
this.searchPrompt.removeEventListener('keyup', this.renderEmoteLists.bind(this)); this.searchPrompt.removeEventListener('keyup', this.renderEmoteLists.bind(this));
this.searchPrompt.addEventListener('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){ toggleSiteEmotes(event){
@ -72,7 +86,7 @@ class emotePanel extends panelObj{
useEmote(emote){ useEmote(emote){
//If we're using this from the active panel //If we're using this from the active panel
if(client.cPanel.activePanel == this){ if(this.client.cPanel.activePanel == this){
//Close it //Close it
this.client.cPanel.hideActivePanel(); this.client.cPanel.hideActivePanel();
} }
@ -81,6 +95,19 @@ class emotePanel extends panelObj{
this.client.chatBox.chatPrompt.value += `[${emote}]`; 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(){ renderEmoteLists(){
var search = this.searchPrompt.value; var search = this.searchPrompt.value;
@ -102,10 +129,10 @@ class emotePanel extends panelObj{
this.renderEmotes(siteEmotes, this.siteEmoteList); this.renderEmotes(siteEmotes, this.siteEmoteList);
this.renderEmotes(chanEmotes, this.chanEmoteList); 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 //Clear out the container
container.innerHTML = ''; container.innerHTML = '';
@ -165,6 +192,23 @@ class emotePanel extends panelObj{
//Set emote title //Set emote title
emoteTitle.innerHTML = `[${emote.name}]`; 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 //Add the emote media to the emote span
emoteDiv.appendChild(emoteMedia); emoteDiv.appendChild(emoteMedia);
//Add title paragraph node //Add title paragraph node