🆙 Mention Now in UI-Config and UITexts and not hardcoded

This commit is contained in:
duckietm
2026-06-04 11:32:55 +02:00
parent a4137af4fe
commit 7007752e91
8 changed files with 182 additions and 134 deletions
@@ -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<ChatInputCommandSelectorViewProps>
}, [ selectedIndex ]);
return (
<div ref={ listRef } className="absolute bottom-full left-0 z-[1070] max-h-[238px] w-full overflow-y-auto rounded-t-[8px] border-2 border-b-0 border-black bg-[#f2f2eb] shadow-[0_-4px_14px_rgba(0,0,0,0.22)]">
<div ref={ listRef } className="absolute bottom-full left-0 w-full bg-[#e8e8e8] border-2 border-black border-b-0 rounded-t-lg max-h-[240px] overflow-y-auto z-[1070]">
{ commands.map((cmd, index) => (
<button
<div
key={ cmd.key }
className={ `flex min-h-[34px] w-full cursor-pointer items-center gap-2 border-b border-[#c6c6bd] px-3 py-1.5 text-left last:border-b-0 ${ index === selectedIndex ? 'bg-[#255d72] text-white' : 'text-black hover:bg-[#dceaf0]' }` }
type="button"
className={ `px-3 py-1.5 cursor-pointer text-sm flex items-center gap-2 ${ index === selectedIndex ? 'bg-[#283F5D] text-white' : 'hover:bg-gray-300' }` }
onClick={ () => onSelect(cmd) }
onMouseEnter={ () => onHover(index) }
>
<span className={ `shrink-0 rounded-[4px] border px-1.5 py-[1px] font-bold ${ index === selectedIndex ? 'border-white/60 bg-white/15' : 'border-[#8ca6b1] bg-white text-[#123b4c]' }` }>:{ cmd.key }</span>
<span className={ `min-w-0 flex-1 truncate text-[12px] ${ index === selectedIndex ? 'text-white/85' : 'text-[#525252]' }` }>{ cmd.description }</span>
</button>
<span className="font-bold">:{ cmd.key }</span>
<span className={ `text-xs ${ index === selectedIndex ? 'text-gray-300' : 'text-gray-500' }` }>{ cmd.description }</span>
</div>
)) }
</div>
);
@@ -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<MentionAliasScope, string> = {
everyone: 'mentions_ui.aliases.everyone',
friends: 'mentions_ui.aliases.friends',
room: 'mentions_ui.aliases.room'
};
const MENTION_ALIAS_DEFAULTS: Record<MentionAliasScope, string[]> = {
everyone: [ 'all', 'everyone', 'tutti' ],
friends: [ 'friends', 'amici' ],
room: [ 'room', 'stanza' ]
};
const MENTION_ALIAS_DESCRIPTION_KEY: Record<MentionAliasScope, string> = {
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<HTMLInputElement>(null);
const { isVisible: commandSelectorVisible, filteredCommands, selectedIndex, setSelectedIndex, moveUp, moveDown, selectCurrent, close: closeCommandSelector } = useChatCommandSelector(chatValue);
const roomUserList = useRoomUserListSnapshot();
const [ mentionSelectedIndex, setMentionSelectedIndex ] = useState<number>(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<ReadonlyArray<{ key: string; scope: MentionAliasScope; description: string }>>(() =>
{
const out: { key: string; scope: MentionAliasScope; description: string }[] = [];
const seen = new Set<string>();
const scopes: MentionAliasScope[] = [ 'everyone', 'friends', 'room' ];
for(const scope of scopes)
{
const list = sanitizeAliasList(
GetConfigurationValue<unknown>(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<MentionSuggestion[]>(() =>
{
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 }
/> }