From 341b91aad120cf31bb0a9c6ff1b6aa2061c9eb96 Mon Sep 17 00:00:00 2001 From: Speng Reb Date: Thu, 21 May 2026 15:03:56 +0200 Subject: [PATCH] Improve UX for emote auto complete --- src/channel/opts.js | 34 +++++++++++++++ templates/channeloptions.pug | 1 + www/js/ui.js | 81 +++++++++++++++++++++++++++++++----- www/js/util.js | 1 + 4 files changed, 106 insertions(+), 11 deletions(-) diff --git a/src/channel/opts.js b/src/channel/opts.js index 500cd5d6..cc5c1fce 100644 --- a/src/channel/opts.js +++ b/src/channel/opts.js @@ -30,6 +30,7 @@ function OptionsModule(_channel) { torbanned: false, // Block connections from Tor exit nodes block_anonymous_users: false, //Only allow connections from registered users. allow_ascii_control: false,// Allow ASCII control characters (\x00-\x1f) + emote_triggers: ":!#/", // Trigger symbols for emote autocomplete playlist_max_per_user: 0, // Maximum number of playlist items per user new_user_chat_delay: 0, // Minimum account/IP age to chat new_user_chat_link_delay: 0, // Minimum account/IP age to post links @@ -353,6 +354,39 @@ OptionsModule.prototype.handleSetOptions = function (user, data) { sendUpdate = true; } + if ("emote_triggers" in data) { + if (typeof data.emote_triggers !== "string") { + user.socket.emit("validationError", { + target: "#cs-emote_triggers", + message: "Emote triggers must be a string of symbols" + }); + } else { + var rawTriggers = data.emote_triggers.trim(); + var normalized = ""; + for (var i = 0; i < rawTriggers.length; i++) { + var ch = rawTriggers.charAt(i); + if (/\s/.test(ch)) { + continue; + } + if (normalized.indexOf(ch) === -1) { + normalized += ch; + } + } + + if (normalized.length === 0) { + normalized = ":!#/"; + } else if (normalized.length > 16) { + normalized = normalized.substring(0, 16); + } + + this.opts.emote_triggers = normalized; + sendUpdate = true; + user.socket.emit("validationPassed", { + target: "#cs-emote_triggers" + }); + } + } + if ("playlist_max_per_user" in data && user.account.effectiveRank >= 3) { var max = parseInt(data.playlist_max_per_user); if (!isNaN(max) && max >= 0) { diff --git a/templates/channeloptions.pug b/templates/channeloptions.pug index 58983e86..c8e81ef5 100644 --- a/templates/channeloptions.pug +++ b/templates/channeloptions.pug @@ -192,6 +192,7 @@ mixin emotes form.form-horizontal(action="javascript:void(0)", role="form") +textbox("cs-emotes-newname", "Emote name") +textbox("cs-emotes-newimage", "Emote image") + +textbox-auto("cs-emote_triggers", "Emote Search Trigger Symbols", ":!#/") .form-group .col-sm-8.col-sm-offset-4 button#cs-emotes-newsubmit.btn.btn-primary Create Emote diff --git a/www/js/ui.js b/www/js/ui.js index 3159960a..150c6eb7 100644 --- a/www/js/ui.js +++ b/www/js/ui.js @@ -167,23 +167,75 @@ function chatTabComplete(chatline) { /* emote autocomplete */ var EMOTE_SUGGEST_IDX = 0; -function emoteLastWord() { - var words = $("#chatline").val().split(" "); - return words[words.length - 1].toLowerCase(); +var EMOTE_SUGGEST_CONTEXT = null; + +function getEmoteTriggerSymbols() { + var raw = (CHANNEL && CHANNEL.opts && CHANNEL.opts.emote_triggers) || ":!#/"; + if (typeof raw !== "string" || raw.length === 0) { + return ":!#/"; + } + return raw; } + +function escapeForRegex(s) { + return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); +} + +function emoteTokenAtCaret() { + var cl = document.getElementById("chatline"); + if (!cl) return null; + var caret = cl.selectionStart; + if (typeof caret !== "number") return null; + + var value = cl.value; + var left = value.slice(0, caret); + var tokenStart = left.lastIndexOf(" ") + 1; + var token = left.slice(tokenStart); + if (!token) return null; + + var triggers = getEmoteTriggerSymbols(); + var triggerClass = escapeForRegex(triggers); + var re = new RegExp("^([" + triggerClass + "])([^\\s]{2,})$"); + var m = token.match(re); + if (!m) return null; + + return { + tokenStart: tokenStart, + trigger: m[1], + query: m[2].toLowerCase() + }; +} + function emoteAccept() { var item = $("#emote-suggestions .active"); if (!item.length) item = $("#emote-suggestions").children().first(); - if (!item.length) return; - var words = $("#chatline").val().split(" "); - words[words.length - 1] = item.data("name") + " "; - $("#chatline").val(words.join(" ")); + if (!item.length || !EMOTE_SUGGEST_CONTEXT) return false; + var cl = document.getElementById("chatline"); + if (!cl) return false; + + var value = cl.value; + var start = EMOTE_SUGGEST_CONTEXT.tokenStart; + var end = cl.selectionStart; + var replacement = item.data("name") + " "; + cl.value = value.slice(0, start) + replacement + value.slice(end); + var newPos = start + replacement.length; + cl.setSelectionRange(newPos, newPos); $("#emote-suggestions").hide(); + EMOTE_SUGGEST_CONTEXT = null; + return true; } function emoteRefresh() { - var partial = emoteLastWord(); + var token = emoteTokenAtCaret(); var popup = $("#emote-suggestions"); - if (partial.length < 2 || !CHANNEL.emotes || !CHANNEL.emotes.length) { popup.hide(); return; } + if (!token || !CHANNEL.emotes || !CHANNEL.emotes.length) { + EMOTE_SUGGEST_CONTEXT = null; + popup.hide(); + return; + } + + var partial = token.query; + EMOTE_SUGGEST_CONTEXT = token; + var matches = CHANNEL.emotes .filter(e => e.name.toLowerCase().includes(partial)) .sort((a, b) => { @@ -226,10 +278,16 @@ $("#chatline").on('keydown', function(ev) { if (open) { if (ev.keyCode == 27) { // Escape $("#emote-suggestions").hide(); + EMOTE_SUGGEST_CONTEXT = null; ev.preventDefault(); return false; + } else if (ev.keyCode == 13) { // Enter accept + if (emoteAccept()) { + ev.preventDefault(); return false; + } } else if (ev.keyCode == 9 || (ev.keyCode == 39 && this.selectionStart === this.value.length)) { // Tab or right arrow at end - emoteAccept(); - ev.preventDefault(); return false; + if (emoteAccept()) { + ev.preventDefault(); return false; + } } else if (ev.keyCode == 38 || ev.keyCode == 40) { // Up/down navigate var items = $("#emote-suggestions").children(); items.eq(EMOTE_SUGGEST_IDX).removeClass("active"); @@ -243,6 +301,7 @@ $("#chatline").on('keydown', function(ev) { // Enter/return if(ev.keyCode == 13) { $("#emote-suggestions").hide(); + EMOTE_SUGGEST_CONTEXT = null; if (CHATTHROTTLE) { return; } diff --git a/www/js/util.js b/www/js/util.js index 8f31603b..30d50dab 100644 --- a/www/js/util.js +++ b/www/js/util.js @@ -990,6 +990,7 @@ function handleModPermissions() { $("#cs-torbanned").prop("checked", CHANNEL.opts.torbanned); $("#cs-block_anonymous_users").prop("checked", CHANNEL.opts.block_anonymous_users); $("#cs-allow_ascii_control").prop("checked", CHANNEL.opts.allow_ascii_control); + $("#cs-emote_triggers").val(CHANNEL.opts.emote_triggers || ":!#/"); $("#cs-playlist_max_per_user").val(CHANNEL.opts.playlist_max_per_user || 0); $("#cs-playlist_max_duration_per_user").val(formatTime(CHANNEL.opts.playlist_max_duration_per_user)); $("#cs-new_user_chat_delay").val(formatTime(CHANNEL.opts.new_user_chat_delay || 0));