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);