5180 fichiers 38.5 Mo 0 téléchargements 306 vues mis à jour il y a 6 heures ui
nitro-v36-with-renderer-v36 / src / hooks / rooms / widgets / useChatMentions.ts
useChatMentions.ts 5 Ko Télécharger le fichier
import { GetSessionDataManager } from '@octane/renderer';
import { RefObject, useCallback, useMemo, useState } from 'react';
import { GetConfigurationValue, LocalizeText } from '../../../api';
import { useRoomUserListSnapshot } from '../../session/useSessionSnapshots';
import {
    buildChatMentionSuggestions,
    computeMentionContext,
    MENTION_ALIAS_CONFIG_KEY,
    MENTION_ALIAS_DEFAULTS,
    MENTION_ALIAS_DESCRIPTION_KEY,
    MentionAlias,
    MentionAliasScope,
    MentionSuggestion,
    sanitizeAliasList
} from './useChatMentions.helpers';

export type { MentionSuggestion, MentionSuggestionKind } from './useChatMentions.helpers';

export interface ChatMentionsState {
    visible: boolean;
    suggestions: MentionSuggestion[];
    selectedIndex: number;
    setSelectedIndex: (index: number) => void;
    moveUp: () => void;
    moveDown: () => void;
    /** Apply the highlighted suggestion (or the first). Returns true if one was applied. */
    applyCurrent: () => boolean;
    apply: (suggestion: MentionSuggestion) => void;
    /** Escape: close the list for this @query and keep what was typed; typing on brings it back. */
    cancel: () => void;
}

/**
 * Chat-input @-mention autocomplete. Owns the caret-context detection, the
 * config-driven broadcast aliases, the room-user suggestion list (minus the
 * viewer), keyboard navigation and the insert/cancel actions — so ChatInputView
 * just wires its keydown handler and the selector view to this hook.
 */
export const useChatMentions = (
    chatValue: string,
    setChatValue: (value: string) => void,
    inputRef: RefObject<HTMLInputElement>,
    commandSelectorVisible: boolean
): ChatMentionsState => {
    const roomUserList = useRoomUserListSnapshot();
    const [selectedIndex, setSelectedIndex] = useState(0);
    const [dismissedKey, setDismissedKey] = useState<string | null>(null);

    const mentionContext = useMemo(() => computeMentionContext(chatValue, commandSelectorVisible), [chatValue, commandSelectorVisible]);

    const aliases = useMemo<MentionAlias[]>(() => {
        const out: MentionAlias[] = [];
        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 suggestions = useMemo<MentionSuggestion[]>(() => {
        if (!mentionContext) return [];

        const session = GetSessionDataManager();

        return buildChatMentionSuggestions(mentionContext.query, roomUserList, aliases, session?.userId ?? -1, session?.userName || '');
    }, [mentionContext, roomUserList, aliases]);

    const contextKey = mentionContext ? `${mentionContext.atIndex}:${mentionContext.query}` : null;
    const visible = suggestions.length > 0 && dismissedKey !== contextKey;
    // Clamp the selection to the current list (derived, no effect) so a
    // shrinking list never leaves the highlight out of range.
    const safeIndex = selectedIndex < suggestions.length ? selectedIndex : 0;

    const apply = useCallback(
        (suggestion: MentionSuggestion) => {
            if (!suggestion || !mentionContext) return;

            const before = chatValue.slice(0, mentionContext.replaceFrom);
            const after = chatValue.slice(mentionContext.replaceTo);
            const inserted = `@${suggestion.insertToken} `;

            setChatValue(`${before}${inserted}${after}`);

            requestAnimationFrame(() => {
                if (!inputRef.current) return;

                const caret = before.length + inserted.length;

                inputRef.current.focus();
                inputRef.current.setSelectionRange(caret, caret);
            });

            setSelectedIndex(0);
        },
        [chatValue, mentionContext, setChatValue, inputRef]
    );

    const moveUp = useCallback(() => setSelectedIndex(safeIndex <= 0 ? suggestions.length - 1 : safeIndex - 1), [safeIndex, suggestions.length]);
    const moveDown = useCallback(() => setSelectedIndex(safeIndex >= suggestions.length - 1 ? 0 : safeIndex + 1), [safeIndex, suggestions.length]);

    const applyCurrent = useCallback(() => {
        const picked = suggestions[safeIndex] ?? suggestions[0];

        if (!picked) return false;

        apply(picked);

        return true;
    }, [suggestions, safeIndex, apply]);

    const cancel = useCallback(() => {
        setSelectedIndex(0);
        setDismissedKey(contextKey);
    }, [contextKey]);

    return { visible, suggestions, selectedIndex: safeIndex, setSelectedIndex, moveUp, moveDown, applyCurrent, apply, cancel };
};
15Sujets
1Dépôts
20Membres
2En ligne
Profile
Inscrit
Dernière visite
Messages
0
Votes positifs
0
Votes négatifs
0

Nous utilisons des cookies pour assurer le bon fonctionnement du site et améliorer votre expérience. en savoir plus

Autoriser