From 20f817506fcff40723197d1305fb361258f42580 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sun, 1 Jun 2025 20:48:17 +0200 Subject: [PATCH 1/4] Refactored message logic --- src/snek/static/chat-input.js | 415 +++++++++++++++------------------- 1 file changed, 182 insertions(+), 233 deletions(-) diff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js index 110ea27..cb7fa47 100644 --- a/src/snek/static/chat-input.js +++ b/src/snek/static/chat-input.js @@ -2,21 +2,25 @@ import { app } from "../app.js"; class ChatInputComponent extends HTMLElement { autoCompletions = { - "example 1": () => {}, - "example 2": () => {}, + "example 1": () => { + }, + "example 2": () => { + }, } hiddenCompletions = { "/starsRender": () => { - app.rpc.starsRender(this.channelUid,this.value.replace("/starsRender ","")) + app.rpc.starsRender(this.channelUid, this.value.replace("/starsRender ", "")) } } users = [] - textarea = null - _value = "" - lastUpdateEvent = null - previousValue = "" - lastChange = null - changed = false + textarea = null + _value = "" + lastUpdateEvent = null + previousValue = "" + lastChange = null + changed = false + expiryTimer = null; + constructor() { super(); this.lastUpdateEvent = new Date(); @@ -32,18 +36,20 @@ class ChatInputComponent extends HTMLElement { } set value(value) { - this._value = value || ""; + this._value = value; this.textarea.value = this._value; } + get allAutoCompletions() { - return Object.assign({},this.autoCompletions,this.hiddenCompletions) + return Object.assign({}, this.autoCompletions, this.hiddenCompletions) } + resolveAutoComplete() { let count = 0; let value = null; - + Object.keys(this.allAutoCompletions).forEach((key) => { - if (key.startsWith(this.value.split(" ")[0])) { + if (key.startsWith(this.value.split(" ")[0])) { count++; value = key; } @@ -58,117 +64,109 @@ class ChatInputComponent extends HTMLElement { focus() { this.textarea.focus(); - } - getAuthors(){ - let authors = [] - for (let i = 0; i < this.users.length; i++) { - authors.push(this.users[i].username) - authors.push(this.users[i].nick) - } - return authors - - } - extractMentions(text) { - const regex = /@([a-zA-Z0-9_-]+)/g; - const mentions = []; - let match; - - while ((match = regex.exec(text)) !== null) { - mentions.push(match[1]); } - return mentions; -} - matchMentionsToAuthors(mentions, authors) { - return mentions.map(mention => { - let closestAuthor = null; - let minDistance = Infinity; - const lowerMention = mention.toLowerCase(); - - authors.forEach(author => { - const lowerAuthor = author.toLowerCase(); - let distance = this.levenshteinDistance(lowerMention, lowerAuthor); - + getAuthors() { + let authors = [] + for (let i = 0; i < this.users.length; i++) { + authors.push(this.users[i].username) + authors.push(this.users[i].nick) + } + return authors - if(!this.isSubsequence(lowerMention,lowerAuthor)) { - distance += 10 + } + + extractMentions(text) { + return Array.from(text.matchAll(/@([a-zA-Z0-9_-]+)/g), m => m[1]); + } + + matchMentionsToAuthors(mentions, authors) { + return mentions.map(mention => { + let closestAuthor = null; + let minDistance = Infinity; + const lowerMention = mention.toLowerCase(); + + authors.forEach(author => { + const lowerAuthor = author.toLowerCase(); + let distance = this.levenshteinDistance(lowerMention, lowerAuthor); + + + if (!this.isSubsequence(lowerMention, lowerAuthor)) { + distance += 10 } - if (distance < minDistance) { - minDistance = distance; - closestAuthor = author; + if (distance < minDistance) { + minDistance = distance; + closestAuthor = author; + } + + }); + + return { mention, closestAuthor, distance: minDistance }; + }); + } + + levenshteinDistance(a, b) { + const matrix = []; + + // Initialize the first row and column + for (let i = 0; i <= b.length; i++) { + matrix[i] = [i]; + } + for (let j = 0; j <= a.length; j++) { + matrix[0][j] = j; + } + + // Fill in the matrix + for (let i = 1; i <= b.length; i++) { + for (let j = 1; j <= a.length; j++) { + if (b.charAt(i - 1) === a.charAt(j - 1)) { + matrix[i][j] = matrix[i - 1][j - 1]; + } else { + matrix[i][j] = Math.min( + matrix[i - 1][j] + 1, // Deletion + matrix[i][j - 1] + 1, // Insertion + matrix[i - 1][j - 1] + 1 // Substitution + ); + } } + } + return matrix[b.length][a.length]; + } + + + replaceMentionsWithAuthors(text) { + const authors = this.getAuthors(); + const mentions = this.extractMentions(text); + + const matches = this.matchMentionsToAuthors(mentions, authors); + let updatedText = text; + matches.forEach(({ mention, closestAuthor }) => { + const mentionRegex = new RegExp(`@${mention}`, 'g'); + updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`); }); - return { mention, closestAuthor, distance: minDistance }; - }); -} -levenshteinDistance(a, b) { - const matrix = []; - - // Initialize the first row and column - for (let i = 0; i <= b.length; i++) { - matrix[i] = [i]; + return updatedText; } - for (let j = 0; j <= a.length; j++) { - matrix[0][j] = j; - } - - // Fill in the matrix - for (let i = 1; i <= b.length; i++) { - for (let j = 1; j <= a.length; j++) { - if (b.charAt(i - 1) === a.charAt(j - 1)) { - matrix[i][j] = matrix[i - 1][j - 1]; - } else { - matrix[i][j] = Math.min( - matrix[i - 1][j] + 1, // Deletion - matrix[i][j - 1] + 1, // Insertion - matrix[i - 1][j - 1] + 1 // Substitution - ); - } - } - } - - return matrix[b.length][a.length]; -} - - - - replaceMentionsWithAuthors(text) { - const authors = this.getAuthors(); - const mentions = this.extractMentions(text); - - const matches = this.matchMentionsToAuthors(mentions, authors); - let updatedText = text; - matches.forEach(({ mention, closestAuthor }) => { - const mentionRegex = new RegExp(`@${mention}`, 'g'); - updatedText = updatedText.replace(mentionRegex, `@${closestAuthor}`); - }); - - return updatedText; -} - async connectedCallback() { - this.user = null + this.user = null app.rpc.getUser(null).then((user) => { - this.user=user + this.user = user }) - - const me = this; this.liveType = this.getAttribute("live-type") === "true"; this.liveTypeInterval = parseInt(this.getAttribute("live-type-interval")) || 6; this.channelUid = this.getAttribute("channel"); - - app.rpc.getRecentUsers(this.channelUid).then(users=>{ - this.users = users + + app.rpc.getRecentUsers(this.channelUid).then(users => { + this.users = users }) - this.messageUid = null; + this.messageUid = null; this.classList.add("chat-input"); @@ -190,18 +188,34 @@ levenshteinDistance(a, b) { this.textarea.addEventListener("keyup", (e) => { if (e.key === "Enter" && !e.shiftKey) { - this.value = ""; + + const message = this.replaceMentionsWithAuthors(this.value); e.target.value = ""; + + if (!message) { + return; + } + let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ", 1)[0]]; + if (autoCompletionHandler) { + autoCompletionHandler(); + this.value = ""; + this.previousValue = ""; + e.target.value = ""; + + return; + } + + this.finalizeMessage() + return; } - this.value = e.target.value; - this.changed = true; - this.update(); + + this.updateFromInput(e.target.value); }); this.textarea.addEventListener("keydown", (e) => { this.value = e.target.value; - let autoCompletion = null; + let autoCompletion = null; if (e.key === "Tab") { e.preventDefault(); autoCompletion = this.resolveAutoComplete(); @@ -213,160 +227,95 @@ levenshteinDistance(a, b) { } if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); - - const message = me.replaceMentionsWithAuthors(this.value); - e.target.value = ""; - - if (!message) { - return; - } - let autoCompletionHandler = this.allAutoCompletions[this.value.split(" ")[0]]; - if (autoCompletionHandler) { - autoCompletionHandler(); - this.value = ""; - this.previousValue = ""; - e.target.value = ""; - - return; - } - - this.updateMessage() - app.rpc.finalizeMessage(this.messageUid) - this.value = ""; - this.previousValue = ""; - this.messageUid = null; } }); - this.changeInterval = setInterval(() => { - if (!this.liveType) { - return; - } - if (this.value !== this.previousValue) { - if ( - this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= - this.liveTypeInterval - ) { - this.value = ""; - this.previousValue = ""; - } - this.lastChange = new Date(); - } - this.update(); - }, 300); - this.addEventListener("upload", (e) => { this.focus(); }); this.addEventListener("uploaded", function (e) { - let message = ""; - e.detail.files.forEach((file) => { - message += `[${file.name}](/channel/attachment/${file.relative_url})`; - }); - app.rpc.sendMessage(this.channelUid, message,true); + let message = e.detail.files.reduce((message, file) => { + return `${message}[${file.name}](/channel/attachment/${file.relative_url})`; + }, ''); + app.rpc.sendMessage(this.channelUid, message, true); }); - setTimeout(()=>{ + setTimeout(() => { this.focus(); - },1000) + }, 1000) } trackSecondsBetweenEvents(event1Time, event2Time) { const millisecondsDifference = event2Time.getTime() - event1Time.getTime(); return millisecondsDifference / 1000; } + isSubsequence(s, t) { - let i = 0, j = 0; - while (i < s.length && j < t.length) { - if (s[i] === t[j]) { - i++; - } - j++; + let i = 0, j = 0; + while (i < s.length && j < t.length) { + if (s[i] === t[j]) { + i++; } - return i === s.length; - } - - - newMessage() { - if (!this.messageUid) { - this.messageUid = "?"; - } - - this.value = this.replaceMentionsWithAuthors(this.value); - this.sendMessage(this.channelUid, this.value,!this.liveType).then((uid) => { - if (this.liveType) { - this.messageUid = uid; - } - }); - } - - updateMessage() { - if (this.value[0] == "/") { - return false; - } - if (!this.messageUid) { - this.newMessage(); - return false; - } - if (this.messageUid === "?") { - return false; - } - if ( - typeof app !== "undefined" && - app.rpc && - typeof app.rpc.updateMessageText === "function" - ) { - app.rpc.updateMessageText(this.messageUid, this.replaceMentionsWithAuthors(this.value)); + j++; } + return i === s.length; } - updateStatus() { - if (this.liveType) { - return; - } - if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) > 1) { + flagTyping() { + if (this.trackSecondsBetweenEvents(this.lastUpdateEvent, new Date()) >= 1) { this.lastUpdateEvent = new Date(); - if ( - typeof app !== "undefined" && - app.rpc && - typeof app.rpc.set_typing === "function" - ) { - app.rpc.set_typing(this.channelUid, this.user.color); + app.rpc.set_typing(this.channelUid, this.user.color).catch(() => { + }); + } + } + + finalizeMessage() { + if (!this.messageUid) { + if (this.value.trim() === "") { + return; + } + this.sendMessage(this.channelUid, this.replaceMentionsWithAuthors(this.value), !this.liveType); + } else { + app.rpc.finalizeMessage(this.messageUid) + } + this.value = ""; + this.previousValue = ""; + this.messageUid = null; + } + + updateFromInput(value) { + if (this.expiryTimer) { + clearTimeout(this.expiryTimer); + this.expiryTimer = null; + } + + this.value = value; + + this.flagTyping() + + if (this.liveType && value[0] !== "/") { + this.expiryTimer = setTimeout(() => { + this.finalizeMessage() + }, this.liveTypeInterval * 1000); + + if (this.messageUid === "?") { + } else if (this.messageUid) { + app.rpc.updateMessageText(this.messageUid, this.replaceMentionsWithAuthors(this.value)); + } else { + this.messageUid = "?"; // Indicate that a message is being sent + this.sendMessage(this.channelUid, this.replaceMentionsWithAuthors(value), !this.liveType).then((uid) => { + if (this.liveType) { + this.messageUid = uid; + } + }); } } } - update() { - const expired = - this.trackSecondsBetweenEvents(this.lastChange, new Date()) >= - this.liveTypeInterval; - const changed = this.value !== this.previousValue; - - if (changed || expired) { - this.lastChange = new Date(); - this.updateStatus(); - } - - this.previousValue = this.value; - - if (this.liveType && expired) { - this.value = ""; - this.previousValue = ""; - this.messageUid = null; - return; - } - - if (changed) { - if (this.liveType) { - this.updateMessage(); - } - } - } - - async sendMessage(channelUid, value,is_final) { + async sendMessage(channelUid, value, is_final) { if (!value.trim()) { return null; } - return await app.rpc.sendMessage(channelUid, value,is_final); + return await app.rpc.sendMessage(channelUid, value, is_final); } } From f7e17080399146e34749901f7b23498149aa18bf Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sun, 1 Jun 2025 20:53:02 +0200 Subject: [PATCH 2/4] Compacted code --- src/snek/static/chat-input.js | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js index cb7fa47..8405e26 100644 --- a/src/snek/static/chat-input.js +++ b/src/snek/static/chat-input.js @@ -67,13 +67,7 @@ class ChatInputComponent extends HTMLElement { } getAuthors() { - let authors = [] - for (let i = 0; i < this.users.length; i++) { - authors.push(this.users[i].username) - authors.push(this.users[i].nick) - } - return authors - + return this.users.flatMap((user) => [user.username, user.nick]) } extractMentions(text) { From 157493b0f4dee5a308308e627e47e33454f375e7 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sun, 1 Jun 2025 20:56:24 +0200 Subject: [PATCH 3/4] Simplified some code --- src/snek/static/chat-input.js | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js index 8405e26..c0ca401 100644 --- a/src/snek/static/chat-input.js +++ b/src/snek/static/chat-input.js @@ -45,17 +45,18 @@ class ChatInputComponent extends HTMLElement { } resolveAutoComplete() { - let count = 0; let value = null; - Object.keys(this.allAutoCompletions).forEach((key) => { - if (key.startsWith(this.value.split(" ")[0])) { - count++; + for (const key of Object.keys(this.allAutoCompletions)) { + if (key.startsWith(this.value.split(" ", 1)[0])) { + if (value) { + return null; + } value = key; } - }); - if (count == 1) return value; - return null; + } + + return value; } isActive() { From deaa7716a2f21d727d01ce0eb8ff18f550d59349 Mon Sep 17 00:00:00 2001 From: BordedDev <> Date: Sun, 1 Jun 2025 20:57:28 +0200 Subject: [PATCH 4/4] Removed some dead code --- src/snek/static/chat-input.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/snek/static/chat-input.js b/src/snek/static/chat-input.js index c0ca401..799faf7 100644 --- a/src/snek/static/chat-input.js +++ b/src/snek/static/chat-input.js @@ -16,9 +16,6 @@ class ChatInputComponent extends HTMLElement { textarea = null _value = "" lastUpdateEvent = null - previousValue = "" - lastChange = null - changed = false expiryTimer = null; constructor() { @@ -26,9 +23,6 @@ class ChatInputComponent extends HTMLElement { this.lastUpdateEvent = new Date(); this.textarea = document.createElement("textarea"); this.value = this.getAttribute("value") || ""; - this.previousValue = this.value; - this.lastChange = new Date(); - this.changed = false; } get value() { @@ -194,7 +188,6 @@ class ChatInputComponent extends HTMLElement { if (autoCompletionHandler) { autoCompletionHandler(); this.value = ""; - this.previousValue = ""; e.target.value = ""; return; @@ -273,7 +266,6 @@ class ChatInputComponent extends HTMLElement { app.rpc.finalizeMessage(this.messageUid) } this.value = ""; - this.previousValue = ""; this.messageUid = null; }