Improve UX for emote auto complete

This commit is contained in:
Speng Reb 2026-05-21 15:03:56 +02:00
parent 914605f393
commit 341b91aad1
4 changed files with 106 additions and 11 deletions

View file

@ -30,6 +30,7 @@ function OptionsModule(_channel) {
torbanned: false, // Block connections from Tor exit nodes torbanned: false, // Block connections from Tor exit nodes
block_anonymous_users: false, //Only allow connections from registered users. block_anonymous_users: false, //Only allow connections from registered users.
allow_ascii_control: false,// Allow ASCII control characters (\x00-\x1f) 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 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_delay: 0, // Minimum account/IP age to chat
new_user_chat_link_delay: 0, // Minimum account/IP age to post links 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; 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) { if ("playlist_max_per_user" in data && user.account.effectiveRank >= 3) {
var max = parseInt(data.playlist_max_per_user); var max = parseInt(data.playlist_max_per_user);
if (!isNaN(max) && max >= 0) { if (!isNaN(max) && max >= 0) {

View file

@ -192,6 +192,7 @@ mixin emotes
form.form-horizontal(action="javascript:void(0)", role="form") form.form-horizontal(action="javascript:void(0)", role="form")
+textbox("cs-emotes-newname", "Emote name") +textbox("cs-emotes-newname", "Emote name")
+textbox("cs-emotes-newimage", "Emote image") +textbox("cs-emotes-newimage", "Emote image")
+textbox-auto("cs-emote_triggers", "Emote Search Trigger Symbols", ":!#/")
.form-group .form-group
.col-sm-8.col-sm-offset-4 .col-sm-8.col-sm-offset-4
button#cs-emotes-newsubmit.btn.btn-primary Create Emote button#cs-emotes-newsubmit.btn.btn-primary Create Emote

View file

@ -167,23 +167,75 @@ function chatTabComplete(chatline) {
/* emote autocomplete */ /* emote autocomplete */
var EMOTE_SUGGEST_IDX = 0; var EMOTE_SUGGEST_IDX = 0;
function emoteLastWord() { var EMOTE_SUGGEST_CONTEXT = null;
var words = $("#chatline").val().split(" ");
return words[words.length - 1].toLowerCase(); 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() { function emoteAccept() {
var item = $("#emote-suggestions .active"); var item = $("#emote-suggestions .active");
if (!item.length) item = $("#emote-suggestions").children().first(); if (!item.length) item = $("#emote-suggestions").children().first();
if (!item.length) return; if (!item.length || !EMOTE_SUGGEST_CONTEXT) return false;
var words = $("#chatline").val().split(" "); var cl = document.getElementById("chatline");
words[words.length - 1] = item.data("name") + " "; if (!cl) return false;
$("#chatline").val(words.join(" "));
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-suggestions").hide();
EMOTE_SUGGEST_CONTEXT = null;
return true;
} }
function emoteRefresh() { function emoteRefresh() {
var partial = emoteLastWord(); var token = emoteTokenAtCaret();
var popup = $("#emote-suggestions"); 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 var matches = CHANNEL.emotes
.filter(e => e.name.toLowerCase().includes(partial)) .filter(e => e.name.toLowerCase().includes(partial))
.sort((a, b) => { .sort((a, b) => {
@ -226,10 +278,16 @@ $("#chatline").on('keydown', function(ev) {
if (open) { if (open) {
if (ev.keyCode == 27) { // Escape if (ev.keyCode == 27) { // Escape
$("#emote-suggestions").hide(); $("#emote-suggestions").hide();
EMOTE_SUGGEST_CONTEXT = null;
ev.preventDefault(); return false; 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 } else if (ev.keyCode == 9 || (ev.keyCode == 39 && this.selectionStart === this.value.length)) { // Tab or right arrow at end
emoteAccept(); if (emoteAccept()) {
ev.preventDefault(); return false; ev.preventDefault(); return false;
}
} else if (ev.keyCode == 38 || ev.keyCode == 40) { // Up/down navigate } else if (ev.keyCode == 38 || ev.keyCode == 40) { // Up/down navigate
var items = $("#emote-suggestions").children(); var items = $("#emote-suggestions").children();
items.eq(EMOTE_SUGGEST_IDX).removeClass("active"); items.eq(EMOTE_SUGGEST_IDX).removeClass("active");
@ -243,6 +301,7 @@ $("#chatline").on('keydown', function(ev) {
// Enter/return // Enter/return
if(ev.keyCode == 13) { if(ev.keyCode == 13) {
$("#emote-suggestions").hide(); $("#emote-suggestions").hide();
EMOTE_SUGGEST_CONTEXT = null;
if (CHATTHROTTLE) { if (CHATTHROTTLE) {
return; return;
} }

View file

@ -990,6 +990,7 @@ function handleModPermissions() {
$("#cs-torbanned").prop("checked", CHANNEL.opts.torbanned); $("#cs-torbanned").prop("checked", CHANNEL.opts.torbanned);
$("#cs-block_anonymous_users").prop("checked", CHANNEL.opts.block_anonymous_users); $("#cs-block_anonymous_users").prop("checked", CHANNEL.opts.block_anonymous_users);
$("#cs-allow_ascii_control").prop("checked", CHANNEL.opts.allow_ascii_control); $("#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_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-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)); $("#cs-new_user_chat_delay").val(formatTime(CHANNEL.opts.new_user_chat_delay || 0));