canopy/www/js/channel/chatPostprocessor.js

400 lines
16 KiB
JavaScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

class chatPostprocessor{
constructor(client){
this.client = client;
}
postprocess(chatEntry, rawData){
this.rawData = rawData;
//Set current chat nodes
this.chatEntry = chatEntry;
this.chatBody = this.chatEntry.querySelector(".chat-entry-body");
//Split the chat message into an array of objects representing each word
this.splitMessage();
//Inject links into un-processed placeholders
this.processLinks();
//Inject clickable command examples
this.processCommandExamples();
//Inject clickable channel names
this.processChannelNames();
//Inject clickable usernames
this.processUsernames();
//Inject whitespace into un-processed words
this.addWhitespace();
//Handle non-standard chat types
this.handleChatType();
//Inject the pre-processed chat into the chatEntry node
this.injectBody();
//Return the pre-processed node
return this.chatEntry;
}
splitMessage(){
//Create an empty array to hold the body
this.messageArray = [];
//First unescape forward-slashes to keep them from splitting, then..
//Split string by word-boundries, with negative lookaheads to exclude file seperators so we don't split link placeholders
const splitString = this.rawData.msg.replaceAll('&#x2F;','/').split(/(?<!␜)\b/g);
//for each word in the splitstring
splitString.forEach((string) => {
//create a word object
const wordObj = {
//re-escape slashes for safety
string: string.replaceAll('/','&#x2F;'),
type: "word"
}
//Add it to our body array
this.messageArray.push(wordObj);
});
}
injectBody(){
//Create an empty array to hold the objects to inject
const injectionArray = [""];
const _this = this;
//For each word object
this.messageArray.forEach((wordObj) => {
if(wordObj.type == 'word'){
//Inject current wordObj string into the chat body
injectString(wordObj.string);
}else if(wordObj.type == 'link'){
//Create a link node from our link
const link = document.createElement('a');
link.classList.add('chat-link');
link.href = wordObj.link;
//Use textContent to be safe since links can't be escaped serverside
link.textContent = wordObj.link;
//Append node to chatBody
injectNode(wordObj, link);
}else if(wordObj.type == 'deadLink'){
//Create a text span node from our link
const badLink = document.createElement('a');
badLink.classList.add('chat-dead-link', 'danger-link');
badLink.href = wordObj.link;
//Use textContent to be safe since links can't be escaped serverside
badLink.textContent = wordObj.link;
//Append node to chatBody
injectNode(wordObj, badLink);
}else if(wordObj.type == 'malformedLink'){
//Create a text span node from our link
const malformedLink = document.createElement('span');
malformedLink.classList.add('chat-malformed-link');
//Use textContent to be safe since links can't be escaped (this is why we don't just add it using injectString)
//arguably we could sanatize malformed links serverside since they're never actually used as links
malformedLink.textContent = wordObj.link;
//Append node to chatBody
injectNode(wordObj, malformedLink);
}else if(wordObj.type == 'image'){
//Create an img node from our link
const img = document.createElement('img');
img.classList.add('chat-img');
img.src = wordObj.link;
//Look for an emote by link since emotes are tx'd as bare links
const emote = this.client.chatBox.commandPreprocessor.getEmoteByLink(wordObj.link);
//If this is a known emote
if(emote != null){
//Set the hover text to the emote's name
img.title = `[${emote.name}]`;
}
//Append node to chatBody
injectNode(wordObj, img);
}else if(wordObj.type == 'video'){
//Create a video node from our link
const vid = document.createElement('video');
vid.classList.add('chat-video');
vid.src = wordObj.link;
vid.controls = false;
vid.autoplay = true;
vid.loop = true;
vid.muted = true;
//Look for an emote by link since emotes are tx'd as bare links
const emote = this.client.chatBox.commandPreprocessor.getEmoteByLink(wordObj.link);
//If this is a known emote
if(emote != null){
//Set the hover text to the emote's name
vid.title = `[${emote.name}]`;
}
injectNode(wordObj, vid);
}else if(wordObj.type == 'command'){
//Create link node
const link = document.createElement('a');
//Set class
link.classList.add('chat-link');
//Set href and inner text
link.href = "javascript:";
link.innerText = wordObj.command;
//Add chatbox functionality
link.addEventListener('click', () => {this.client.chatBox.commandPreprocessor.preprocess(wordObj.command)});
//We don't have to worry about injecting this into whitespace since there shouldn't be any here.
injectionArray.push(link);
}else if(wordObj.type == "username"){
//Create link node
const link = document.createElement('a');
//set class
link.classList.add(wordObj.color);
//Set href and inner text
link.href = "javascript:";
link.innerText = wordObj.string;
//add chatbox functionality
link.addEventListener('click', () => {this.client.chatBox.chatPrompt.value += `${wordObj.string} `});
//We don't have to worry about injecting this into whitespace since there shouldn't be any here.
injectionArray.push(link);
}else if(wordObj.type == "channel"){
//Create link node
const link = document.createElement('a');
//set class
link.classList.add('chat-link');
//Set href and inner text
link.href = `/c/${wordObj.chan}`;
link.innerText = wordObj.string;
//We don't have to worry about injecting this into whitespace since there shouldn't be any here.
injectionArray.push(link);
}else{
console.warn("Unknown chat postprocessor word type:");
console.warn(wordObj);
}
});
//For each item found in the injection array
injectionArray.forEach((item) => {
//if it's a string
if(typeof item == "string"){
//Create span (can't add to innerHTML without clobbering sibling DOM nodes)
const text = document.createElement('span');
//Set text to innerHTML (can't just append as a text node since the message was escaped server-side)
text.innerHTML = item;
this.chatBody.appendChild(text);
//Otherwise it should be a DOM node
}else{
//Append the node to our chat body
this.chatBody.appendChild(item);
}
})
//Like string.replace except it actually injects the node so we can keep things like event handlers
function injectNode(wordObj, node, placeholder = '␜'){
//Split string by the placeholder so we can keep surrounding whitespace
const splitWord = wordObj.string.split(placeholder, 2);
//Append the first half of the string
injectString(splitWord[0]);
//Append the node
injectionArray.push(node);
//Append the second half of the string
injectString(splitWord[1]);
}
function injectString(string){
//If the last item was a string
if(typeof injectionArray[injectionArray.length - 1] == "string"){
//add the word string on to the end of the string
injectionArray[injectionArray.length - 1] += string;
}else{
//Pop the string at the end of the array
injectionArray.push(string);
}
}
}
processCommandExamples(){
//for each word object in the body
this.messageArray.forEach((wordObj, wordIndex) => {
//if the word object hasn't been pre-processed elsewhere
if(wordObj.type == "word"){
//Get last char of current word with slashes unescaped
const unescaped = wordObj.string.replaceAll('&#x2F;','/')
const lastChar = unescaped[unescaped.length - 1];
//if the last char is !
if(lastChar == '!' || lastChar == '/'){
//get next word
const nextWord = this.messageArray[wordIndex + 1];
//if we have another word
if(nextWord != null){
const command = lastChar + nextWord.string;
//Take out the command marker
this.messageArray[wordIndex].string = unescaped.slice(0,-1);
const commandObj = {
type: "command",
string: nextWord.string,
command: command
}
this.messageArray[wordIndex + 1] = commandObj;
}
}
}
});
}
processChannelNames(){
//for each word object in the body
this.messageArray.forEach((wordObj, wordIndex) => {
//if the word object hasn't been pre-processed elsewhere
if(wordObj.type == "word"){
//Get last char of current word with slashes unescaped
const lastChar = wordObj.string[wordObj.string.length - 1];
const secondLastChar = wordObj.string[wordObj.string.length - 2];
//if the last char is # and the second to last char isn't & (avoid escaped HTML char codes)
if(lastChar == '#' && secondLastChar != '&'){
//get next word
const nextWord = this.messageArray[wordIndex + 1];
//if we have another word
if(nextWord != null){
//Take out the chan marker
this.messageArray[wordIndex].string = wordObj.string.slice(0,-1);
const commandObj = {
type: "channel",
string: lastChar + nextWord.string,
chan: nextWord.string
}
this.messageArray[wordIndex + 1] = commandObj;
}
}
}
});
}
processUsernames(){
//for each word object in the body
this.messageArray.forEach((wordObj, wordIndex) => {
//if the word object hasn't been pre-processed elsewhere
if(wordObj.type == "word"){
//Check for user and get their color
const color = this.client.userList.colorMap.get(wordObj.string);
//If the current word is the username of a connected user
if(color != null){
//Mark it as so
this.messageArray[wordIndex].type = "username";
//Store their color
this.messageArray[wordIndex].color = color;
}
}
});
}
addWhitespace(){
//for each word object in the body
this.messageArray.forEach((wordObj, wordIndex) => {
//if the word object hasn't been pre-processed elsewhere
if(wordObj.type == "word"){
//Create an empty array to hold our word
var wordArray = [];
//For each character in the string of the current word object
this.messageArray[wordIndex].string.split("").forEach((char, charIndex) => {
//push the current character to the wordArray
wordArray.push(char);
//After eight characters
if(charIndex > 8){
//Push an invisible line-break character between every character
wordArray.push("");
}
});
//Join the wordArray into a single string, and use it to set the current wordObject's string
this.messageArray[wordIndex].string = wordArray.join("");
}
});
}
processLinks(){
//If we don't have links
if(this.rawData.links == null){
//Don't bother
return;
}
//For every link received in this message
this.rawData.links.forEach((link, linkIndex) => {
//For every word obj in the message array
this.messageArray.forEach((wordObj, wordIndex) => {
//Check current wordobj for link (placeholder may contain whitespace with it)
if(wordObj.string.match(`${linkIndex}`)){
//Set current word object in the body array to the new link object
this.messageArray[wordIndex] = {
//Don't want to use a numbered placeholder to make this easier during body injection
//but we also don't want to clobber any surrounding whitespace
string: wordObj.string.replace(`${linkIndex}`, '␜'),
link: link.link,
type: link.type
}
}
})
});
}
handleChatType(){
if(this.rawData.type == "whisper"){
//add whisper class
this.chatBody.classList.add('whisper');
}else if(this.rawData.type == "announcement"){
//Squash the high-level
this.chatEntry.querySelector('.high-level').remove();
//Get the username and make it into an announcement title (little hacky but this *IS* postprocessing)
const userNode = this.chatEntry.querySelector('.chat-entry-username');
userNode.innerHTML = `${userNode.innerHTML.slice(0,-2)} Announcement`;
//Add/remove relevant classes
userNode.classList.remove('chat-entry-username');
userNode.classList.add('announcement-title');
this.chatBody.classList.add('announcement-body');
this.chatEntry.classList.add('announcement');
}else if(this.rawData.type == "toke"){
//Squash the high-level
this.chatEntry.querySelector('.high-level').remove();
//remove the username
this.chatEntry.querySelector('.chat-entry-username').remove();
//Add toke/tokewhisper class
this.chatBody.classList.add("toke");
}else if(this.rawData.type == "tokewhisper"){
//Squash the high-level
this.chatEntry.querySelector('.high-level').remove();
//remove the username
this.chatEntry.querySelector('.chat-entry-username').remove();
//Add toke/tokewhisper class
this.chatBody.classList.add("tokewhisper","serverwhisper");
}
}
}