Merge pull request #13 from Spengreb/emote-substring-search-ux

Emote substring search ux
This commit is contained in:
Spengreb 2026-05-21 15:06:22 +02:00 committed by GitHub
commit 03922e8484
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 115 additions and 14 deletions

View file

@ -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) {

View file

@ -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

View file

@ -167,26 +167,84 @@ 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; }
var matches = CHANNEL.emotes.filter(function(e) {
return e.name.toLowerCase().indexOf(partial) === 0;
}).slice(0, 8);
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) => {
const an = a.name.toLowerCase();
const bn = b.name.toLowerCase();
return (bn.startsWith(partial) - an.startsWith(partial)) || an.localeCompare(bn);
})
.slice(0, 8);
if (!matches.length) { popup.hide(); return; }
popup.empty();
matches.forEach(function(e) {
@ -220,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");
@ -237,6 +301,7 @@ $("#chatline").on('keydown', function(ev) {
// Enter/return
if(ev.keyCode == 13) {
$("#emote-suggestions").hide();
EMOTE_SUGGEST_CONTEXT = null;
if (CHATTHROTTLE) {
return;
}

View file

@ -995,6 +995,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));