From 7007752e9167d025ff2149767921d5e27d5110bf Mon Sep 17 00:00:00 2001 From: duckietm Date: Thu, 4 Jun 2026 11:32:55 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=86=99=20Mention=20Now=20in=20UI-Config?= =?UTF-8?q?=20and=20UITexts=20and=20not=20hardcoded?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- public/configuration/UITexts_en.json5.example | 43 +++++--- public/configuration/UITexts_it.json5.example | 43 +++++--- public/configuration/UITexts_nl.json5.example | 41 ++++--- src/App.tsx | 3 - .../RoomWidgetUpdateChatInputContentEvent.ts | 1 - .../ChatInputCommandSelectorView.tsx | 18 ++- .../room/widgets/chat-input/ChatInputView.tsx | 104 ++++++++++++------ .../rooms/widgets/useChatCommandSelector.ts | 63 ++++++----- 8 files changed, 182 insertions(+), 134 deletions(-) diff --git a/public/configuration/UITexts_en.json5.example b/public/configuration/UITexts_en.json5.example index 3bfbe4b..9a91bca 100644 --- a/public/configuration/UITexts_en.json5.example +++ b/public/configuration/UITexts_en.json5.example @@ -640,6 +640,8 @@ 'wheel.extra': 'Extra spins: %count%', 'wheel.spin': 'SPIN', 'wheel.buy': 'Buy spin', + 'wheel.settings': 'Settings', + 'wheel.settings.title': 'Wheel of Fortune Settings', 'wheel.winners': 'Latest winners', 'wheel.winners.empty': 'No winners yet', 'wheel.win.title': 'You won!', @@ -707,24 +709,6 @@ 'chatcmd.client.info': 'Client info', // ------------------------------------------------------------------------ - // Mentions - // ------------------------------------------------------------------------ - 'mentions.window.title': 'Mentions', - 'mentions.window.empty': 'No mentions', - 'mentions.window.markall': 'Mark all as read', - 'mentions.tab.title': 'Mentions', - 'mentions.notification': '%sender% mentioned you in %room%', - 'mentions.filter.all': 'All', - 'mentions.filter.unread': 'Unread', - 'mentions.filter.direct': 'Direct', - 'mentions.filter.room': 'Room', - 'mentions.group.today': 'Today', - 'mentions.group.yesterday': 'Yesterday', - 'mentions.group.older': 'Earlier', - 'mentions.type.direct': 'Direct mention', - 'mentions.type.room': 'Room mention', - 'mentions.action.goto': 'Go to room', - 'mentions.action.remove': 'Remove', // Me-menu settings + User account settings window // ------------------------------------------------------------------------ 'usersettings.tab.general': "General", @@ -790,4 +774,27 @@ 'usersettings.success.password': "Password updated successfully.", 'usersettings.success.email': "Email updated successfully.", 'usersettings.success.username': "Username updated. Please log in again with your new name.", + + // ------------------------------------------------------------------------ + // @-mention autocomplete (chat input) + // ------------------------------------------------------------------------ + 'mentions.window.title': "Mentions", + 'mentions.window.empty': "No mentions", + 'mentions.window.markall': "Mark all as read", + 'mentions.tab.title': "Mentions", + 'mentions.notification': "%sender% mentioned you in %room%", + 'mentions.filter.all': "All", + 'mentions.filter.unread': "Unread", + 'mentions.filter.direct': "Direct", + 'mentions.filter.room': "Room", + 'mentions.group.today': "Today", + 'mentions.group.yesterday': "Yesterday", + 'mentions.group.older': "Earlier", + 'mentions.type.direct': "Direct mention", + 'mentions.type.room': "Room mention", + 'mentions.action.goto': "Go to room", + 'mentions.action.remove': "Remove", + 'mentions.alias.description.everyone': "Everyone in the hotel", + 'mentions.alias.description.friends': "Your online friends", + 'mentions.alias.description.room': "Everyone in this room", } diff --git a/public/configuration/UITexts_it.json5.example b/public/configuration/UITexts_it.json5.example index 593498e..81bd966 100644 --- a/public/configuration/UITexts_it.json5.example +++ b/public/configuration/UITexts_it.json5.example @@ -640,6 +640,8 @@ 'wheel.extra': 'Giri extra: %count%', 'wheel.spin': 'GIRA', 'wheel.buy': 'Acquista giro', + 'wheel.settings': 'Configurações', + 'wheel.settings.title': 'Configuração de Sistema da Roleta', 'wheel.winners': 'Ultimi vincitori', 'wheel.winners.empty': 'Ancora nessun vincitore', 'wheel.win.title': 'Hai vinto!', @@ -707,24 +709,6 @@ 'chatcmd.client.info': 'Info client', // ------------------------------------------------------------------------ - // Mentions - // ------------------------------------------------------------------------ - 'mentions.window.title': 'Menzioni', - 'mentions.window.empty': 'Nessuna menzione', - 'mentions.window.markall': 'Segna tutte come lette', - 'mentions.tab.title': 'Menzioni', - 'mentions.notification': '%sender% ti ha menzionato in %room%', - 'mentions.filter.all': 'Tutte', - 'mentions.filter.unread': 'Non lette', - 'mentions.filter.direct': 'Dirette', - 'mentions.filter.room': 'Stanza', - 'mentions.group.today': 'Oggi', - 'mentions.group.yesterday': 'Ieri', - 'mentions.group.older': 'Precedenti', - 'mentions.type.direct': 'Menzione diretta', - 'mentions.type.room': 'Menzione di stanza', - 'mentions.action.goto': 'Vai alla stanza', - 'mentions.action.remove': 'Rimuovi', // Me-menu settings + User account settings window // ------------------------------------------------------------------------ 'usersettings.tab.general': "Generale", @@ -790,4 +774,27 @@ 'usersettings.success.password': "Password aggiornata con successo.", 'usersettings.success.email': "Email aggiornata con successo.", 'usersettings.success.username': "Nome utente aggiornato. Accedi di nuovo con il tuo nuovo nome.", + + // ------------------------------------------------------------------------ + // @-mention autocomplete (chat input) + // ------------------------------------------------------------------------ + 'mentions.window.title': "Menzioni", + 'mentions.window.empty': "Nessuna menzione", + 'mentions.window.markall': "Segna tutte come lette", + 'mentions.tab.title': "Menzioni", + 'mentions.notification': "%sender% ti ha menzionato in %room%", + 'mentions.filter.all': "Tutte", + 'mentions.filter.unread': "Non lette", + 'mentions.filter.direct': "Dirette", + 'mentions.filter.room': "Stanza", + 'mentions.group.today': "Oggi", + 'mentions.group.yesterday': "Ieri", + 'mentions.group.older': "Precedenti", + 'mentions.type.direct': "Menzione diretta", + 'mentions.type.room': "Menzione di stanza", + 'mentions.action.goto': "Vai alla stanza", + 'mentions.action.remove': "Rimuovi", + 'mentions.alias.description.everyone': "Tutti nell'hotel", + 'mentions.alias.description.friends': "I tuoi amici online", + 'mentions.alias.description.room': "Tutti in questa stanza", } diff --git a/public/configuration/UITexts_nl.json5.example b/public/configuration/UITexts_nl.json5.example index 62f963f..ebd3616 100644 --- a/public/configuration/UITexts_nl.json5.example +++ b/public/configuration/UITexts_nl.json5.example @@ -709,24 +709,6 @@ 'chatcmd.client.info': 'Client info', // ------------------------------------------------------------------------ - // Mentions - // ------------------------------------------------------------------------ - 'mentions.window.title': 'Vermeldingen', - 'mentions.window.empty': 'Geen vermeldingen', - 'mentions.window.markall': 'Alles als gelezen markeren', - 'mentions.tab.title': 'Vermeldingen', - 'mentions.notification': '%sender% heeft je genoemd in %room%', - 'mentions.filter.all': 'Alle', - 'mentions.filter.unread': 'Ongelezen', - 'mentions.filter.direct': 'Direct', - 'mentions.filter.room': 'Kamer', - 'mentions.group.today': 'Vandaag', - 'mentions.group.yesterday': 'Gisteren', - 'mentions.group.older': 'Eerder', - 'mentions.type.direct': 'Directe vermelding', - 'mentions.type.room': 'Kamervermelding', - 'mentions.action.goto': 'Ga naar kamer', - 'mentions.action.remove': 'Verwijderen', // Me-menu settings + User account settings window // ------------------------------------------------------------------------ 'usersettings.tab.general': "Algemeen", @@ -792,4 +774,27 @@ 'usersettings.success.password': "Wachtwoord succesvol bijgewerkt.", 'usersettings.success.email': "E-mail succesvol bijgewerkt.", 'usersettings.success.username': "Gebruikersnaam bijgewerkt. Log opnieuw in met je nieuwe naam.", + + // ------------------------------------------------------------------------ + // @-mention autocomplete (chat input) + // ------------------------------------------------------------------------ + 'mentions.window.title': "Vermeldingen", + 'mentions.window.empty': "Geen vermeldingen", + 'mentions.window.markall': "Markeer alles als gelezen", + 'mentions.tab.title': "Vermeldingen", + 'mentions.notification': "%sender% noemde je in %room%", + 'mentions.filter.all': "Alles", + 'mentions.filter.unread': "Ongelezen", + 'mentions.filter.direct': "Direct", + 'mentions.filter.room': "Kamer", + 'mentions.group.today': "Vandaag", + 'mentions.group.yesterday': "Gisteren", + 'mentions.group.older': "Eerder", + 'mentions.type.direct': "Directe vermelding", + 'mentions.type.room': "Kamervermelding", + 'mentions.action.goto': "Ga naar kamer", + 'mentions.action.remove': "Verwijderen", + 'mentions.alias.description.everyone': "Iedereen in het hotel", + 'mentions.alias.description.friends': "Je vrienden online", + 'mentions.alias.description.room': "Iedereen in deze kamer", } diff --git a/src/App.tsx b/src/App.tsx index babffe1..4b799ef 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -7,7 +7,6 @@ import { LoginView } from './components/login/LoginView'; import { MainView } from './components/MainView'; import { ReconnectView } from './components/reconnect/ReconnectView'; import { useMessageEvent, useNitroEvent } from './hooks'; -import { ensureChatCommandListener } from './hooks/rooms/widgets/useChatCommandSelector'; NitroVersion.UI_VERSION = GetUIVersion(); @@ -563,9 +562,7 @@ export const App: FC<{}> = props => bumpProgress(85, taskLabel('loading.task.rooms', 'Loading rooms...')); await GetRoomEngine().init(); bumpProgress(92, taskLabel('loading.task.engine', 'Loading graphics engine...')); - ensureChatCommandListener(); await GetCommunication().init(); - ensureChatCommandListener(); bumpProgress(98, taskLabel('generic.reconnecting', 'Connecting to server...')); })(); } diff --git a/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts b/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts index aea7193..9352372 100644 --- a/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts +++ b/src/api/room/events/RoomWidgetUpdateChatInputContentEvent.ts @@ -5,7 +5,6 @@ export class RoomWidgetUpdateChatInputContentEvent extends RoomWidgetUpdateEvent public static CHAT_INPUT_CONTENT: string = 'RWUCICE_CHAT_INPUT_CONTENT'; public static WHISPER: string = 'whisper'; public static SHOUT: string = 'shout'; - public static TEXT: string = 'text'; private _chatMode: string = ''; private _userName: string = ''; diff --git a/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx index d3e0eae..5eae3d2 100644 --- a/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputCommandSelectorView.tsx @@ -1,10 +1,9 @@ import { FC, useEffect, useRef } from 'react'; -import type { CommandDefinition } from '../../../../api'; -import type { RankedCommandDefinition } from '../../../../hooks/rooms/widgets/useChatCommandSelector.helpers'; +import { CommandDefinition } from '../../../../api'; interface ChatInputCommandSelectorViewProps { - commands: RankedCommandDefinition[]; + commands: CommandDefinition[]; selectedIndex: number; onSelect: (command: CommandDefinition) => void; onHover: (index: number) => void; @@ -25,18 +24,17 @@ export const ChatInputCommandSelectorView: FC }, [ selectedIndex ]); return ( -
+
{ commands.map((cmd, index) => ( - + :{ cmd.key } + { cmd.description } +
)) }
); diff --git a/src/components/room/widgets/chat-input/ChatInputView.tsx b/src/components/room/widgets/chat-input/ChatInputView.tsx index 08ffe8e..85d3198 100644 --- a/src/components/room/widgets/chat-input/ChatInputView.tsx +++ b/src/components/room/widgets/chat-input/ChatInputView.tsx @@ -12,15 +12,40 @@ import { ChatInputStyleSelectorView } from './ChatInputStyleSelectorView'; const USER_TYPE_REAL_USER = 1; const MAX_MENTION_SUGGESTIONS = 8; -const MENTION_ALIASES: ReadonlyArray<{ key: string; label: string; description?: string }> = [ - { key: 'all', label: 'all', description: 'Everyone in the hotel' }, - { key: 'everyone', label: 'everyone', description: 'Everyone in the hotel' }, - { key: 'tutti', label: 'tutti', description: 'Everyone in the hotel' }, - { key: 'friends', label: 'friends', description: 'Your online friends' }, - { key: 'amici', label: 'amici', description: 'Your online friends' }, - { key: 'room', label: 'room', description: 'Everyone in this room' }, - { key: 'stanza', label: 'stanza', description: 'Everyone in this room' } -]; + +type MentionAliasScope = 'everyone' | 'friends' | 'room'; + +const MENTION_ALIAS_CONFIG_KEY: Record = { + everyone: 'mentions_ui.aliases.everyone', + friends: 'mentions_ui.aliases.friends', + room: 'mentions_ui.aliases.room' +}; + +const MENTION_ALIAS_DEFAULTS: Record = { + everyone: [ 'all', 'everyone', 'tutti' ], + friends: [ 'friends', 'amici' ], + room: [ 'room', 'stanza' ] +}; + +const MENTION_ALIAS_DESCRIPTION_KEY: Record = { + everyone: 'mentions.alias.description.everyone', + friends: 'mentions.alias.description.friends', + room: 'mentions.alias.description.room' +}; + +const sanitizeAliasList = (raw: unknown, fallback: string[]): string[] => +{ + if(!Array.isArray(raw)) return fallback; + const out: string[] = []; + for(const entry of raw) + { + if(typeof entry !== 'string') continue; + const trimmed = entry.trim(); + if(!trimmed) continue; + out.push(trimmed); + } + return out; +}; export const ChatInputView: FC<{}> = props => { @@ -30,6 +55,7 @@ export const ChatInputView: FC<{}> = props => const { roomSession = null } = useRoom(); const inputRef = useRef(null); const { isVisible: commandSelectorVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close: closeCommandSelector } = useChatCommandSelector(chatValue); + const roomUserList = useRoomUserListSnapshot(); const [ mentionSelectedIndex, setMentionSelectedIndex ] = useState(0); @@ -46,12 +72,38 @@ export const ChatInputView: FC<{}> = props => if(at > 0 && !/\s/.test(upToCaret.charAt(at - 1))) return null; const query = upToCaret.slice(at + 1); - if(/\s/.test(query)) return null; return { atIndex: at, replaceFrom: at, replaceTo: caret, query }; }, [ chatValue, commandSelectorVisible ]); + const mentionAliases = useMemo>(() => + { + const out: { key: string; scope: MentionAliasScope; description: string }[] = []; + const seen = new Set(); + + const scopes: MentionAliasScope[] = [ 'everyone', 'friends', 'room' ]; + for(const scope of scopes) + { + const list = sanitizeAliasList( + GetConfigurationValue(MENTION_ALIAS_CONFIG_KEY[scope], MENTION_ALIAS_DEFAULTS[scope]), + MENTION_ALIAS_DEFAULTS[scope] + ); + + for(const key of list) + { + const lower = key.toLowerCase(); + + if(seen.has(lower)) continue; + seen.add(lower); + + out.push({ key, scope, description: LocalizeText(MENTION_ALIAS_DESCRIPTION_KEY[scope]) }); + } + } + + return out; + }, []); + const mentionSuggestions = useMemo(() => { if(!mentionContext) return []; @@ -76,14 +128,14 @@ export const ChatInputView: FC<{}> = props => if(out.length >= MAX_MENTION_SUGGESTIONS) break; } - for(const alias of MENTION_ALIASES) + for(const alias of mentionAliases) { if(query.length > 0 && !alias.key.toLowerCase().startsWith(query)) continue; out.push({ key: `alias:${ alias.key }`, kind: 'alias', - name: alias.label, + name: alias.key, insertToken: alias.key, description: alias.description }); @@ -92,7 +144,7 @@ export const ChatInputView: FC<{}> = props => } return out; - }, [ mentionContext, roomUserList ]); + }, [ mentionContext, roomUserList, mentionAliases ]); const mentionSelectorVisible = mentionSuggestions.length > 0; @@ -148,23 +200,6 @@ export const ChatInputView: FC<{}> = props => inputRef.current.setSelectionRange((inputRef.current.value.length * 2), (inputRef.current.value.length * 2)); }, [ inputRef ]); - const setChatInputValue = useCallback((value: string, markTyping: boolean = true) => - { - setChatValue(value); - - if(markTyping) - { - setIsTyping(!!value.length); - setIsIdle(!!value.length); - } - - requestAnimationFrame(() => - { - inputRef.current?.focus(); - inputRef.current?.setSelectionRange(value.length, value.length); - }); - }, [ setIsTyping, setIsIdle ]); - const checkSpecialKeywordForInput = useCallback(() => { setChatValue(prevValue => @@ -279,7 +314,7 @@ export const ChatInputView: FC<{}> = props => if(selected) { event.preventDefault(); - setChatInputValue(':' + selected.key + ' '); + setChatValue(':' + selected.key + ' '); return; } break; @@ -319,7 +354,7 @@ export const ChatInputView: FC<{}> = props => case 'Escape': event.preventDefault(); setMentionSelectedIndex(0); - + if(mentionContext) { const before = chatValue.slice(0, mentionContext.replaceFrom); @@ -361,9 +396,6 @@ export const ChatInputView: FC<{}> = props => { switch(event.chatMode) { - case RoomWidgetUpdateChatInputContentEvent.TEXT: - setChatInputValue(event.userName); - return; case RoomWidgetUpdateChatInputContentEvent.WHISPER: { setChatValue(`${ chatModeIdWhisper } ${ event.userName } `); return; @@ -450,7 +482,7 @@ export const ChatInputView: FC<{}> = props => selectedIndex={ selectedIndex } onSelect={ (cmd) => { - setChatInputValue(':' + cmd.key + ' '); + setChatValue(':' + cmd.key + ' '); inputRef.current?.focus(); } } onHover={ setSelectedIndex } /> } diff --git a/src/hooks/rooms/widgets/useChatCommandSelector.ts b/src/hooks/rooms/widgets/useChatCommandSelector.ts index 55785a2..c999dec 100644 --- a/src/hooks/rooms/widgets/useChatCommandSelector.ts +++ b/src/hooks/rooms/widgets/useChatCommandSelector.ts @@ -3,9 +3,6 @@ import { useCallback, useEffect, useMemo, useState } from 'react'; import { CommandDefinition, LocalizeText } from '../../../api'; import { createNitroStore } from '../../../state/createNitroStore'; import { useMessageEvent } from '../../events'; -import { getChatCommandQuery, getRankedCommandSuggestions } from './useChatCommandSelector.helpers'; - -const MAX_VISIBLE_COMMANDS = 8; // Client-only commands are static; safe to keep at module scope. The // `descriptionKey` is a LocalizeText slot resolved at merge time so @@ -65,7 +62,7 @@ const useChatCommandStore = createNitroStore()((set) => ({ markListenerRegistered: () => set({ isListenerRegistered: true }) })); -export const ensureChatCommandListener = (): void => +const ensureGlobalListener = (): void => { if(useChatCommandStore.getState().isListenerRegistered) return; @@ -89,20 +86,20 @@ export const ensureChatCommandListener = (): void => // Try once at module load so the server's response landing before any // React mount still hits the cache. -ensureChatCommandListener(); +ensureGlobalListener(); export const useChatCommandSelector = (chatValue: string) => { const serverCommands = useChatCommandStore(s => s.serverCommands); const setServerCommands = useChatCommandStore(s => s.setServerCommands); const [ selectedIndex, setSelectedIndex ] = useState(0); - const [ dismissedQuery, setDismissedQuery ] = useState(null); + const [ dismissed, setDismissed ] = useState(false); useEffect(() => { // Cover the case where the module-level registration failed // because GetCommunication() wasn't ready at import time. - ensureChatCommandListener(); + ensureGlobalListener(); }, []); // Late updates (rank change, etc.) — go through the store so all @@ -126,55 +123,61 @@ export const useChatCommandSelector = (chatValue: string) => return merged.sort((a, b) => a.key.localeCompare(b.key)); }, [ serverCommands ]); - const filterText = useMemo(() => getChatCommandQuery(chatValue), [ chatValue ]); + const filterText = useMemo(() => + { + if(!chatValue.startsWith(':') || chatValue.includes(' ')) return ''; + + return chatValue.slice(1).toLowerCase(); + }, [ chatValue ]); const filteredCommands = useMemo(() => { - if(filterText === null) return []; + if(!filterText && !chatValue.startsWith(':')) return []; - return getRankedCommandSuggestions(allCommands, filterText, MAX_VISIBLE_COMMANDS); - }, [ allCommands, filterText ]); + return allCommands.filter(cmd => cmd.key.toLowerCase().startsWith(filterText)); + }, [ allCommands, filterText, chatValue ]); const isVisible = useMemo(() => { - return filterText !== null && filteredCommands.length > 0 && dismissedQuery !== filterText; - }, [ filterText, filteredCommands, dismissedQuery ]); - - const boundedSelectedIndex = useMemo(() => - { - if(!filteredCommands.length) return 0; - - return Math.min(selectedIndex, filteredCommands.length - 1); - }, [ filteredCommands.length, selectedIndex ]); + return chatValue.startsWith(':') && !chatValue.includes(' ') && filteredCommands.length > 0 && !dismissed; + }, [ chatValue, filteredCommands, dismissed ]); const moveUp = useCallback(() => { - if(!filteredCommands.length) return; - - setSelectedIndex(prev => ((prev <= 0 || prev >= filteredCommands.length) ? filteredCommands.length - 1 : prev - 1)); + setSelectedIndex(prev => (prev <= 0 ? filteredCommands.length - 1 : prev - 1)); }, [ filteredCommands.length ]); const moveDown = useCallback(() => { - if(!filteredCommands.length) return; - setSelectedIndex(prev => (prev >= filteredCommands.length - 1 ? 0 : prev + 1)); }, [ filteredCommands.length ]); const selectCurrent = useCallback((): CommandDefinition | null => { - if(boundedSelectedIndex >= 0 && boundedSelectedIndex < filteredCommands.length) + if(selectedIndex >= 0 && selectedIndex < filteredCommands.length) { - return filteredCommands[boundedSelectedIndex]; + return filteredCommands[selectedIndex]; } return null; - }, [ boundedSelectedIndex, filteredCommands ]); + }, [ selectedIndex, filteredCommands ]); const close = useCallback(() => { - setDismissedQuery(filterText); + setDismissed(true); + }, []); + + // Reset dismissed when chatValue changes to a new command start + useEffect(() => + { + if(chatValue === ':' || chatValue === '') setDismissed(false); + }, [ chatValue ]); + + // Reset selectedIndex when filtered list changes + useEffect(() => + { + setSelectedIndex(0); }, [ filterText ]); - return { isVisible, filteredCommands, selectedIndex: boundedSelectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close }; + return { isVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close }; };