canopy/www/doc/client/chat.js.html

672 lines
24 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>JSDoc: Source: chat.js</title>
<script src="scripts/prettify/prettify.js"> </script>
<script src="scripts/prettify/lang-css.js"> </script>
<!--[if lt IE 9]>
<script src="//html5shiv.googlecode.com/svn/trunk/html5.js"></script>
<![endif]-->
<link type="text/css" rel="stylesheet" href="styles/prettify-tomorrow.css">
<link type="text/css" rel="stylesheet" href="styles/jsdoc-default.css">
</head>
<body>
<div id="main">
<h1 class="page-title">Source: chat.js</h1>
<section>
<article>
<pre class="prettyprint source linenums"><code>/*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 &lt;https://www.gnu.org/licenses/>.*/
/**
* Class which represents Canopy Chat Box UI
*/
class chatBox{
/**
* Instantiates a new Chat Box object
* @param {channel} client - Parent client Management Object
*/
constructor(client){
/**
* Parent Client Management Object
*/
this.client = client
/**
* Whether or not chat-size should be locked to current media aspect ratio
*/
this.aspectLock = true;
/**
* Whether or not the chat box should auto-scroll on new chat
*/
this.autoScroll = true;
/**
* Chat Buffer Scroll Top on last scroll
*/
this.lastPos = 0;
/**
* Height of Chat Buffer on last scroll
*/
this.lastHeight = 0;
/**
* Width of Chat Buffer on last scroll
*/
this.lastWidth = 0;
/**
* Click-Dragger Object for handling dynamic chat/video split re-sizing
*/
this.clickDragger = new canopyUXUtils.clickDragger("#chat-panel-drag-handle", "#chat-panel-div");
/**
* Command Pre-Processor Object
*/
this.commandPreprocessor = new commandPreprocessor(client);
/**
* Chat Post-Processor Object
*/
this.chatPostprocessor = new chatPostprocessor(client);
//Element Nodes
/**
* Chat Panel Container Div
*/
this.chatPanel = document.querySelector("#chat-panel-div");
/**
* High Level Selector
*/
this.highSelect = document.querySelector("#chat-panel-high-level-select");
/**
* Flair Selector
*/
this.flairSelect = document.querySelector("#chat-panel-flair-select");
/**
* Chat Buffer Div
*/
this.chatBuffer = document.querySelector("#chat-panel-buffer-div");
/**
* Chat Prompt
*/
this.chatPrompt = document.querySelector("#chat-panel-prompt");
/**
* Auto-Complete Placeholder
*/
this.autocompletePlaceholder = document.querySelector("#chat-panel-prompt-autocomplete-filler");
/**
* Auto-Complete Display
*/
this.autocompleteDisplay = document.querySelector("#chat-panel-prompt-autocomplete-display");
/**
* Settings Panel Icon
*/
this.settingsIcon = document.querySelector("#chat-panel-settings-icon");
/**
* Admin Panel Icon
*/
this.adminIcon = document.querySelector("#chat-panel-admin-icon");
/**
* Emote Icon
*/
this.emoteIcon = document.querySelector("#chat-panel-emote-icon");
/**
* Send Chat/Command Button
*/
this.sendButton = document.querySelector("#chat-panel-send-button");
/**
* Aspect Lock Icon
* Seems weird to stick this in here, but the split is dictated by chat width :P
*/
this.aspectLockIcon = document.querySelector("#media-panel-aspect-lock-icon");
/**
* Hide Chat Icon
*/
this.hideChatIcon = document.querySelector("#chat-panel-div-hide");
/**
* Show Chat Icon
*/
this.showChatIcon = document.querySelector("#media-panel-show-chat-icon");
//Setup functions
this.setupInput();
this.defineListeners();
this.sizeToAspect();
}
/**
* Defines input-related event listeners
*/
setupInput(){
//Chat bar
this.chatPrompt.addEventListener("keydown", this.send.bind(this));
this.chatPrompt.addEventListener("keydown", this.tabComplete.bind(this));
this.chatPrompt.addEventListener("input", this.displayAutocomplete.bind(this));
this.autocompleteDisplay.addEventListener("click", this.tabComplete.bind(this));
this.sendButton.addEventListener("click", this.send.bind(this));
this.settingsIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new settingsPanel(client))});
this.adminIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new queuePanel(client))});
this.emoteIcon.addEventListener("click", ()=>{this.client.cPanel.setActivePanel(new emotePanel(client))});
//Header icons
this.aspectLockIcon.addEventListener("click", this.lockAspect.bind(this));
this.showChatIcon.addEventListener("click", ()=>{this.toggleUI()});
this.hideChatIcon.addEventListener("click", ()=>{this.toggleUI()});
this.highSelect.addEventListener("change", this.setHighLevel.bind(this));
this.flairSelect.addEventListener("change", this.setFlair.bind(this));
//Clickdragger/Resize
this.clickDragger.handle.addEventListener("mousedown", this.unlockAspect.bind(this));
this.clickDragger.handle.addEventListener("clickdrag", this.handleAutoScroll.bind(this));
window.addEventListener("resize", this.resizeAspect.bind(this));
//chatbuffer
this.chatBuffer.addEventListener('scroll', this.scrollHandler.bind(this));
}
/**
* Defines network-related event listners
*/
defineListeners(){
this.client.socket.on("chatMessage", this.displayChat.bind(this));
this.client.socket.on("clearChat", this.clearChat.bind(this));
}
/**
* Clears chat on command from server
* @param {Object} data - Data from server
*/
clearChat(data){
//If we where passed a user to check
if(data.user != null){
var clearedChats = document.querySelectorAll(`.chat-entry-${data.user}`);
}else{
var clearedChats = document.querySelectorAll('.chat-entry');
}
//For each chat found
clearedChats.forEach((chat) => {
//fuckin' nukem!
chat.remove();
});
}
/**
* Receives, Post-Processes, and Displays chat messages from server
* @param {Object} data De-hydrated chat object from server
*/
displayChat(data){
//Create chat-entry span
var chatEntry = document.createElement('span');
chatEntry.classList.add("chat-panel-buffer","chat-entry",`chat-entry-${data.user}`);
//Create high-level label
var highLevel = document.createElement('p');
highLevel.classList.add("chat-panel-buffer","chat-entry-high-level","high-level");
highLevel.textContent = utils.unescapeEntities(`${data.highLevel}`);
chatEntry.appendChild(highLevel);
//If we're not using classic flair
if(data.flair != "classic"){
//Use flair
var flair = `flair-${data.flair}`;
//Otherwise
}else{
//Pull user's assigned color from the color map
var flair = this.client.userList.colorMap.get(data.user);
}
//Create username label
var userLabel = document.createElement('p');
userLabel.classList.add("chat-panel-buffer", "chat-entry-username", );
//Create color span
var flairSpan = document.createElement('span');
flairSpan.classList.add("chat-entry-flair-span", flair);
flairSpan.innerHTML = data.user;
//Inject flair span into user label before the colon
userLabel.innerHTML = `${flairSpan.outerHTML}: `;
//Append user label
chatEntry.appendChild(userLabel);
//Create chat body
var chatBody = document.createElement('p');
chatBody.classList.add("chat-panel-buffer","chat-entry-body");
chatEntry.appendChild(chatBody);
//Append the post-processed chat-body to the chat buffer
this.chatBuffer.appendChild(this.chatPostprocessor.postprocess(chatEntry, data));
//Set size to aspect on launch
this.resizeAspect();
}
/**
* Concatenate Text into Chat Prompt
* @param {String} text - Text to Concatenate
*/
catChat(text){
this.chatPrompt.value += text;
this.displayAutocomplete();
}
/**
* Calls a toke command out with a specified user
* @param {String} user - User to toke with
*/
tokeWith(user){
this.commandPreprocessor.preprocess(user == this.client.user.user ? "!toke up fuckers" : `!toke up ${user}`);
}
/**
* Pre-processes and sends text from chat prompt to server
* @param {Event} event - Event passed down from Event Handler
*/
send(event){
if((!event || !event.key || event.key == "Enter") &amp;&amp; this.chatPrompt.value){
this.commandPreprocessor.preprocess(this.chatPrompt.value);
//Clear our prompt and autocomplete nodes
this.chatPrompt.value = "";
this.autocompletePlaceholder.innerHTML = '';
this.autocompleteDisplay.innerHTML = '';
}
}
/**
* Displays auto-complete text against current prompt input
* @param {Event} event - Event passed down from Event Handler
*/
displayAutocomplete(event){
//Find current match
const match = this.checkAutocomplete();
//Set placeholder to space out the autocomplete display
//Use text content because it's unescaped, and while this only effects local users, it'll keep someone from noticing and whinging about it
this.autocompletePlaceholder.textContent = this.chatPrompt.value;
//Set the autocomplete display
this.autocompleteDisplay.textContent = match.match.replace(match.word, '');
}
/**
* Called upon tab-complete
* @param {Event} event - Event passed down from Event Handler
*/
tabComplete(event){
//If we hit tab or this isn't a keyboard event
if(event.key == "Tab" || event.key == null){
//Prevent default action
event.preventDefault();
//return focus to the chat prompt
this.chatPrompt.focus();
//Grab autocompletion match
const match = this.checkAutocomplete();
//If we have a match
if(match.match != ''){
//Autocomplete the current word
this.chatPrompt.value += match.match.replace(match.word, '');
//Clear out the autocomplete display
this.autocompleteDisplay.innerHTML = '';
}
}
}
/**
* Checks string input against auto-complete dictionary to generate the best guess as to what the user is typing
* @param {String} input - Current input from Chat Prompt
* @returns {Object} Object containing word we where handed and the match we found
*/
checkAutocomplete(input = this.chatPrompt.value){
//Rebuild this fucker every time because it really doesn't take that much compute power and emotes/used tokes change
//Worst case we could store it persistantly and update as needed but I think that might be much
const dictionary = this.commandPreprocessor.buildAutocompleteDictionary();
//Split our input by whitespace
const splitInput = input.split(/\s/g);
//Get the current word we're working on
const word = splitInput[splitInput.length - 1];
let matches = [];
//Run through dictionary sets
for(let set of Object.keys(dictionary)){
//Go through the current definitions of the current dictionary set
//I went with a for loop instead of a filter beacuse I wanted to pull the processed definition with pre/postfix
//and also directly push it into a shared array :P
for(let cmd of dictionary[set].cmds){
//Append the proper prefix/postfix to the current command
const definition = (`${dictionary[set].prefix}${cmd[0]}${dictionary[set].postfix}`);
//if definition starts with the current word and the command is enabled
if((word == '' ? false : definition.indexOf(word) == 0) &amp;&amp; cmd[1]){
//Add definition to match list
matches.push(definition);
}
}
}
//If we found jack shit
if(matches.length == 0){
//Return jack shit
return {
match: '',
word
};
//If we got something
}else{
//return our top match
return {
match: matches[0],
word
};
}
}
/**
* Handles initial client meta-data dump from server upon connection
* @param {Object} data - Data dump from server
*/
handleClientInfo(data){
this.updateFlairSelect(data.flairList, data.user.flair);
this.updateHighSelect(data.user.highLevel);
}
/**
* Sets user high-level
* @param {Event} event - Event passed down from Event Handler
*/
setHighLevel(event){
const highLevel = event.target.value;
this.client.socket.emit("setHighLevel", {highLevel});
}
/**
* Sets user flair
* @param {Event} event - Event passed down from Event Handler
*/
setFlair(event){
const flair = event.target.value;
this.client.socket.emit("setFlair", {flair});
}
/**
* Handles High-Level updates from the server
* @param {Number} highLevel - High Level to Set
*/
updateHighSelect(highLevel){
this.highSelect.value = highLevel;
}
/**
* Handles flair updates from the server
* @param {Array} fliarList - List of flairs to put into flair select
* @param {String} fliar - Flair to set
*/
updateFlairSelect(flairList, flair){
//clear current flair select
this.flairSelect.innerHTML = "";
//For each flair in flairlist
flairList.forEach((flair) => {
//Create an option
var flairOption = document.createElement('option');
//Set the name and innerHTML
flairOption.value = flair.name;
flairOption.textContent = utils.unescapeEntities(flair.displayName);
//Append it to the select
this.flairSelect.appendChild(flairOption);
});
//Set the selected flair in the UI
this.flairSelect.value = flair;
//Re-style the UI, do this in two seperate steps in-case we're running for the first time and have nothing to replace.
this.flairSelect.className = this.flairSelect.className.replace(/flair-\S*/, "");
this.flairSelect.classList.add(`flair-${flair}`);
}
/**
* Locks chat-size to aspect ratio of media
* @param {Event} event - Event passed down from Event Handler
*/
lockAspect(event){
//prevent the user from breaking shit :P
if(this.chatPanel.style.display != "none"){
this.aspectLock = true;
this.aspectLockIcon.style.display = "none";
this.sizeToAspect();
}
}
/**
* Un-locks chat-size to aspect ratio of media
* @param {Event} event - Event passed down from Event Handler
*/
unlockAspect(event){
//Disable aspect lock
this.aspectLock = false;
//Show aspect lock icon
this.aspectLockIcon.style.display = "inline";
}
L /**
* Re-sizes chat back to aspect ratio on window re-size when chat box is aspect locked
* Also prevents horizontal scroll-bars from chat/window resizing
* @param {Event} event - Event passed down from Event Handler
*/
resizeAspect(event){
const playerHidden = this.client.player.playerDiv.style.display == "none";
//If the aspect is locked and the player is hidden
if(this.aspectLock &amp;&amp; !playerHidden){
this.sizeToAspect();
//Otherwise
}else{
//Fix the clickDragger on userlist
this.client.userList.clickDragger.fixCutoff();
}
//Autoscroll chat in-case we fucked it up
this.handleAutoScroll();
}
L /**
* Re-sizes chat box relative to media aspect ratio
*/
sizeToAspect(){
if(this.chatPanel.style.display != "none"){
var targetVidWidth = this.client.player.getRatio() * this.chatPanel.getBoundingClientRect().height;
const targetChatWidth = window.innerWidth - targetVidWidth;
//This should be changeable in settings later on, for now it defaults to 20%
const limit = window.innerWidth * .2;
//Set width to target or 20vh depending on whether or not we've hit the width limit
this.chatPanel.style.flexBasis = targetChatWidth > limit ? `${targetChatWidth}px` : '20vh';
//Fix busted layout
var pageBreak = document.body.scrollWidth - document.body.getBoundingClientRect().width;
this.chatPanel.style.flexBasis = `${this.chatPanel.getBoundingClientRect().width + pageBreak}px`;
//This sometimes gets called before userList ahs been initiated :p
if(this.client.userList != null){
this.client.userList.clickDragger.fixCutoff();
}
}
}
/**
* Toggles Chat Box UX
* @param {Boolean} show - Whether or not to show Chat Box UX
*/
toggleUI(show = !this.chatPanel.checkVisibility()){
if(show){
this.chatPanel.style.display = "flex";
this.showChatIcon.style.display = "none";
this.client.player.hideVideoIcon.style.display = "flex";
this.client.userList.clickDragger.fixCutoff();
}else{
this.chatPanel.style.display = "none";
this.showChatIcon.style.display = "flex";
this.client.player.hideVideoIcon.style.display = "none";
}
}
/**
* Handles Video Toggling
* @param {Boolean} show - Whether or not the video is currently being hidden
*/
handleVideoToggle(show){
//If we're enabling the video
if(show){
//Show hide chat icon
this.hideChatIcon.style.display = "flex";
//Re-enable the click dragger
this.clickDragger.enabled = true;
//Lock the chat to aspect ratio of the video, to make sure the chat width isn't breaking shit
this.lockAspect();
//If we're disabling the video
}else{
//Hide hide hide hide hide hide chat icon
this.hideChatIcon.style.display = "none";
//Need to clear the width from the split, or else it doesn't display properly
this.chatPanel.style.flexBasis = "100%";
//Disable the click dragger
this.clickDragger.enabled = false;
}
}
/**
* Handles scrolling within the chat buffer
* @param {Event} event - Event passed down from Event Handler
*/
scrollHandler(event){
//If we're just starting out
if(this.lastPos == 0){
//Set last pos for the first time
this.lastPos = this.chatBuffer.scrollTop;
}
//Calculate scroll delta
const deltaY = this.chatBuffer.scrollTop - this.lastPos;
//Grab visible bounding rect so we don't have to do it again (can't use offset because someone might zoom in :P)
const bufferRect = this.chatBuffer.getBoundingClientRect();
const bufferHeight = Math.round(bufferRect.height);
const bufferWidth = Math.round(bufferRect.width);
if(this.lastHeight == 0){
this.lastHeight = bufferHeight;
}
if(this.lastWidth == 0){
this.lastWidth = bufferWidth;
}
//If we're scrolling up
if(deltaY &lt; 0){
//If we have room to scroll, and we didn't resize
if(this.chatBuffer.scrollHeight > bufferHeight &amp;&amp; (this.lastWidth == bufferWidth &amp;&amp; this.lastHeight == bufferHeight)){
//Disable auto scrolling
this.autoScroll = false;
}else{
this.handleAutoScroll();
}
//Otherwise if the difference between the chat buffers scroll height and offset height is equal to the scroll top
//(Because it is scrolled all the way down)
}else if((this.chatBuffer.scrollHeight - bufferHeight) == this.chatBuffer.scrollTop){
this.autoScroll = true;
}
//Set last post/size for next the run
this.lastPos = this.chatBuffer.scrollTop;
this.lastHeight = bufferHeight;
this.lastWidth = bufferWidth;
}
/**
* Auto-scrolls chat buffer when new chats are entered.
*/
handleAutoScroll(){
//If autoscroll is enabled
if(this.autoScroll){
//Set chatBuffer scrollTop to the difference between scrollHeight and buffer height (scroll to the bottom)
this.chatBuffer.scrollTop = this.chatBuffer.scrollHeight - Math.round(this.chatBuffer.getBoundingClientRect().height);
}
}
}</code></pre>
</article>
</section>
</div>
<nav>
<h2><a href="index.html">Home</a></h2><h3>Classes</h3><ul><li><a href="addURLPopup.html">addURLPopup</a></li><li><a href="cPanel.html">cPanel</a></li><li><a href="channel.html">channel</a></li><li><a href="chatBox.html">chatBox</a></li><li><a href="chatPostprocessor.html">chatPostprocessor</a></li><li><a href="clearPopup.html">clearPopup</a></li><li><a href="commandPreprocessor.html">commandPreprocessor</a></li><li><a href="commandProcessor.html">commandProcessor</a></li><li><a href="defaultTitlesPopup.html">defaultTitlesPopup</a></li><li><a href="emotePanel.html">emotePanel</a></li><li><a href="hlsBase.html">hlsBase</a></li><li><a href="hlsLiveStreamHandler.html">hlsLiveStreamHandler</a></li><li><a href="mediaHandler.html">mediaHandler</a></li><li><a href="newPlaylistPopup.html">newPlaylistPopup</a></li><li><a href="nullHandler.html">nullHandler</a></li><li><a href="panelObj.html">panelObj</a></li><li><a href="player.html">player</a></li><li><a href="playlistManager.html">playlistManager</a></li><li><a href="poppedPanel.html">poppedPanel</a></li><li><a href="queuePanel.html">queuePanel</a></li><li><a href="rawFileBase.html">rawFileBase</a></li><li><a href="rawFileHandler.html">rawFileHandler</a></li><li><a href="renamePopup.html">renamePopup</a></li><li><a href="reschedulePopup.html">reschedulePopup</a></li><li><a href="schedulePopup.html">schedulePopup</a></li><li><a href="settingsPanel.html">settingsPanel</a></li><li><a href="userList.html">userList</a></li><li><a href="youtubeEmbedHandler.html">youtubeEmbedHandler</a></li></ul><h3>Global</h3><ul><li><a href="global.html#onYouTubeIframeAPIReady">onYouTubeIframeAPIReady</a></li></ul>
</nav>
<br class="clear">
<footer>
Documentation generated by <a href="https://github.com/jsdoc/jsdoc">JSDoc 4.0.4</a> on Sat Sep 06 2025 00:47:15 GMT-0400 (Eastern Daylight Time)
</footer>
<script> prettyPrint(); </script>
<script src="scripts/linenumber.js"> </script>
</body>
</html>