Files
Nitro-V3/src/hooks/mentions/useMentionAutocomplete.ts
T
medievalshell 0df810c556 feat(mentions): @ autocomplete, blue @nick, avatar notification toast
- Chat input @ autocomplete: typing @ shows online users (room users +
  online friends + room aliases) with avatars; arrows/Tab/Enter to pick.
- Any valid @nick token is highlighted blue in chat bubbles (like @all),
  giving visual feedback that it is a recognised mention.
- Side notification toast on a received mention: sender avatar (from the
  new senderFigure wire field) + message + dismiss; dismiss marks it read
  so the toolbar unread badge updates. Auto-hides after 8s.
- IMentionEntry/parsers carry senderFigure end to end.
2026-06-04 01:18:26 +02:00

90 lines
3.0 KiB
TypeScript

import { useEffect, useMemo, useState } from 'react';
import { MENTION_ROOM_ALIASES } from '../../components/room/widgets/chat/highlightMentions';
import { useFriendsState } from '../friends/useFriends';
import { useRoomUserListSnapshot } from '../session/useSessionSnapshots';
export interface MentionSuggestion
{
name: string;
figure: string;
isAlias: boolean;
}
const MAX_SUGGESTIONS = 8;
// Trova il token @<parziale> che si sta digitando alla FINE del valore.
// Restituisce il parziale (anche '' subito dopo @) oppure null se non si è in un @mention.
const activeMentionPartial = (value: string): string | null =>
{
if(!value || value.indexOf('@') < 0) return null;
const match = /(?:^|\s)@([A-Za-z0-9_]*)$/.exec(value);
return match ? match[1] : null;
};
export interface MentionAutocompleteState
{
isVisible: boolean;
suggestions: MentionSuggestion[];
selectedIndex: number;
setSelectedIndex: (index: number) => void;
moveUp: () => void;
moveDown: () => void;
current: () => MentionSuggestion | null;
// Inserisce il nome scelto sostituendo il parziale @... alla fine del valore.
applyTo: (value: string, name: string) => string;
}
export const useMentionAutocomplete = (chatValue: string): MentionAutocompleteState =>
{
const roomUsers = useRoomUserListSnapshot();
const { onlineFriends } = useFriendsState();
const [ selectedIndex, setSelectedIndex ] = useState(0);
const partial = useMemo(() => activeMentionPartial(chatValue), [ chatValue ]);
const suggestions = useMemo<MentionSuggestion[]>(() =>
{
if(partial === null) return [];
const query = partial.toLowerCase();
const seen = new Set<string>();
const out: MentionSuggestion[] = [];
const add = (name: string, figure: string, isAlias: boolean) =>
{
if(!name || out.length >= MAX_SUGGESTIONS) return;
const key = name.toLowerCase();
if(seen.has(key)) return;
if(query && !key.startsWith(query)) return;
seen.add(key);
out.push({ name, figure: figure || '', isAlias });
};
for(const user of (roomUsers || [])) add(user?.name, (user as any)?.figure, false);
for(const friend of (onlineFriends || [])) add(friend?.name, friend?.figure, false);
for(const alias of MENTION_ROOM_ALIASES) add(alias, '', true);
return out;
}, [ partial, roomUsers, onlineFriends ]);
useEffect(() => { setSelectedIndex(0); }, [ partial ]);
const isVisible = (partial !== null) && (suggestions.length > 0);
return {
isVisible,
suggestions,
selectedIndex,
setSelectedIndex,
moveUp: () => setSelectedIndex(index => (index <= 0 ? suggestions.length - 1 : index - 1)),
moveDown: () => setSelectedIndex(index => (index >= suggestions.length - 1 ? 0 : index + 1)),
current: () => suggestions[selectedIndex] ?? null,
applyTo: (value: string, name: string) => value.replace(/@([A-Za-z0-9_]*)$/, '@' + name + ' ')
};
};