diff --git a/www/js/channel/chat.js b/www/js/channel/chat.js index 5a5bb23..c634eff 100644 --- a/www/js/channel/chat.js +++ b/www/js/channel/chat.js @@ -125,7 +125,7 @@ class chatBox{ chatEntry.appendChild(chatBody); - this.chatBuffer.appendChild(this.chatPostprocessor.preprocess(chatEntry, data)); + this.chatBuffer.appendChild(this.chatPostprocessor.postprocess(chatEntry, data)); //Set size to aspect on launch this.resizeAspect(); diff --git a/www/js/channel/chatPostprocessor.js b/www/js/channel/chatPostprocessor.js index 8000da7..36ae396 100644 --- a/www/js/channel/chatPostprocessor.js +++ b/www/js/channel/chatPostprocessor.js @@ -3,7 +3,7 @@ class chatPostprocessor{ this.client = client; } - preprocess(chatEntry, rawData){ + postprocess(chatEntry, rawData){ this.rawData = rawData; //Set current chat nodes this.chatEntry = chatEntry; @@ -15,6 +15,15 @@ class chatPostprocessor{ //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(); @@ -30,14 +39,17 @@ class chatPostprocessor{ 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.split(/(? { //create a word object const wordObj = { - string: string, + //re-escape slashes for safety + string: string.replaceAll('/','/'), type: "word" } @@ -123,15 +135,62 @@ class chatPostprocessor{ } 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"){ - //Add it to the chat's innerHTML (it should already be escaped by the server) - this.chatBody.innerHTML += item; + //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 @@ -167,6 +226,88 @@ class chatPostprocessor{ } } + 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('/','/') + 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) => {