5180 fichiers 38.5 Mo 0 téléchargements 296 vues mis à jour il y a 6 heures ui
nitro-v36-with-renderer-v36 / src / hooks / rooms / widgets / useChatWidget.ts
useChatWidget.ts 17.4 Ko Télécharger le fichier
import {
    GetGuestRoomResultEvent,
    GetRoomEngine,
    GetSessionDataManager,
    PetFigureData,
    RoomChatSettings,
    RoomChatSettingsEvent,
    RoomDragEvent,
    RoomObjectCategory,
    RoomObjectType,
    RoomObjectVariable,
    RoomSessionChatEvent,
    RoomUserData,
    SystemChatStyleEnum
} from '@octane/renderer';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
import { useUserChatPreferencesStore } from '@/state/userChatPreferences';
import {
    applyUserChatPreferences,
    CHAT_MODE_FREE_FLOW,
    ChatBubbleMessage,
    ChatBubbleUtilities,
    ChatEntryType,
    ChatHistoryCurrentDate,
    GetConfigurationRecord,
    GetConfigurationValue,
    GetRoomObjectScreenLocation,
    IRoomChatSettings,
    loadEmojiShortcodes,
    LocalizeText,
    PlaySound,
    resolveChatMode,
    RoomChatFormatter
} from '../../../api';
import { getStoredChatTextSize } from '../../../components/room/widgets/chat-input/chatTextSize';
import { useChatHistory } from './../../chat-history';
import { useMessageEvent, useOctaneEvent } from '../../events';
import { useUserDataSnapshot } from '../../session/useSessionSnapshots';
import { useTranslation } from '../../translation';
import { useRoom } from '../useRoom';

const CHAT_MESSAGES_MAX = 250;

const useChatWidgetState = () => {
    const [chatMessages, setChatMessages] = useState<ChatBubbleMessage[]>([]);
    const [chatSettings, setChatSettings] = useState<IRoomChatSettings>({
        mode: RoomChatSettings.CHAT_MODE_FREE_FLOW,
        weight: RoomChatSettings.CHAT_BUBBLE_WIDTH_NORMAL,
        speed: RoomChatSettings.CHAT_SCROLL_SPEED_NORMAL,
        distance: 50,
        protection: RoomChatSettings.FLOOD_FILTER_NORMAL
    });
    const { roomSession = null } = useRoom();
    const { addChatEntry, updateChatEntry } = useChatHistory();
    const { settings, translateIncoming, consumeOutgoingTranslation } = useTranslation();
    const isDisposed = useRef(false);
    // Reactive: re-renders if the session-data snapshot flips (e.g.
    // reconnect under a different user id). Safe to call here —
    // useChatWidget is not singleton-backed (see export below),
    // so the real React dispatcher is in scope and
    // useSyncExternalStore installs correctly.
    const ownUserId = useUserDataSnapshot().userId || -1;

    const applyTranslationToBubble = useCallback(
        (chatMessage: ChatBubbleMessage, originalText: string, translatedText: string, detectedLanguage: string, targetLanguage: string) => {
            const resolvedOriginalText = originalText || chatMessage.text || '';
            const resolvedTranslatedText = translatedText || resolvedOriginalText;
            const originalFormattedText = RoomChatFormatter(resolvedOriginalText);
            const translatedFormattedText = RoomChatFormatter(resolvedTranslatedText);

            chatMessage.text = resolvedOriginalText;
            chatMessage.formattedText = originalFormattedText;
            chatMessage.originalText = resolvedOriginalText;
            chatMessage.originalFormattedText = originalFormattedText;
            chatMessage.translatedText = resolvedTranslatedText;
            chatMessage.translatedFormattedText = translatedFormattedText;
            chatMessage.translationDetectedLanguage = detectedLanguage || '';
            chatMessage.translationTargetLanguage = targetLanguage || '';
            chatMessage.showTranslation = true;
        },
        []
    );

    const buildTranslatedEntryPatch = useCallback((originalText: string, translatedText: string, detectedLanguage: string, targetLanguage: string) => {
        const resolvedOriginalText = originalText || '';
        const resolvedTranslatedText = translatedText || resolvedOriginalText;

        return {
            showTranslation: true,
            message: RoomChatFormatter(resolvedOriginalText),
            originalMessage: RoomChatFormatter(resolvedOriginalText),
            translatedMessage: RoomChatFormatter(resolvedTranslatedText),
            detectedLanguage: detectedLanguage || '',
            targetLanguage: targetLanguage || ''
        };
    }, []);

    const applyAsyncTranslation = useCallback(
        (bubbleId: number, chatEntryId: number, originalText: string, translatedText: string, detectedLanguage: string, targetLanguage: string) => {
            setChatMessages((prevValue) => {
                const index = prevValue.findIndex((chat) => chat.id === bubbleId);

                if (index === -1) return prevValue;

                // A new object, so the memoized bubble view sees the translation.
                const bubble = Object.assign(Object.create(Object.getPrototypeOf(prevValue[index])), prevValue[index]);
                const newValue = [...prevValue];

                applyTranslationToBubble(bubble, originalText, translatedText, detectedLanguage, targetLanguage);
                newValue[index] = bubble;

                return newValue;
            });

            updateChatEntry(chatEntryId, buildTranslatedEntryPatch(originalText, translatedText, detectedLanguage, targetLanguage));
        },
        [applyTranslationToBubble, buildTranslatedEntryPatch, updateChatEntry]
    );

    const userBubbleWidth = useUserChatPreferencesStore((state) => state.bubbleWidth);
    const userScrollSpeed = useUserChatPreferencesStore((state) => state.scrollSpeed);
    const userChatMode = useUserChatPreferencesStore((state) => state.chatMode);
    const chatMode = resolveChatMode(chatSettings?.mode ?? CHAT_MODE_FREE_FLOW, userChatMode);
    const effectiveChatSettings = useMemo(
        () => applyUserChatPreferences(chatSettings, userBubbleWidth, userScrollSpeed),
        [chatSettings, userBubbleWidth, userScrollSpeed]
    );

    const getScrollSpeed = useMemo(() => {
        if (!effectiveChatSettings) return 6000;

        switch (effectiveChatSettings.speed) {
            case RoomChatSettings.CHAT_SCROLL_SPEED_FAST:
                return 3000;
            case RoomChatSettings.CHAT_SCROLL_SPEED_NORMAL:
                return 6000;
            case RoomChatSettings.CHAT_SCROLL_SPEED_SLOW:
                return 12000;
        }
    }, [effectiveChatSettings]);

    // Synchronous on purpose: the bubble and the history entry go in the order the messages arrived. The head image
    // is downloaded afterwards and put on the bubble when it is ready; awaiting it first let a message from a user
    // whose figure was cached overtake an earlier one.
    useOctaneEvent<RoomSessionChatEvent>(RoomSessionChatEvent.CHAT_EVENT, (event) => {
        const roomObject = GetRoomEngine().getRoomObject(roomSession.roomId, event.objectId, RoomObjectCategory.UNIT);
        const bubbleLocation = roomObject ? GetRoomObjectScreenLocation(roomSession.roomId, roomObject?.id, RoomObjectCategory.UNIT) : { x: 0, y: 0 };
        const userData = roomObject ? roomSession.userDataManager.getUserDataByIndex(event.objectId) : new RoomUserData(-1);

        // The unit is in the room but its user data is not (yet): there is no one to put the bubble on.
        if (!userData) return;

        let username = '';
        let avatarColor = 0;
        const imageUrl: string = null;
        let imageRequest: Promise<string> = null;
        let chatType = event.chatType;
        let styleId = event.style;
        let userType = 0;
        let petType = -1;
        let text = event.message;

        if (userData) {
            userType = userData.type;

            const figure = userData.figure;

            switch (userType) {
                case RoomObjectType.PET:
                    imageRequest = ChatBubbleUtilities.getPetImage(figure, 2, true, 64, roomObject.model.getValue<string>(RoomObjectVariable.FIGURE_POSTURE));
                    petType = new PetFigureData(figure).typeId;
                    break;
                case RoomObjectType.USER:
                    imageRequest = ChatBubbleUtilities.getUserImage(figure);
                    break;
                case RoomObjectType.RENTABLE_BOT:
                case RoomObjectType.BOT:
                    styleId = SystemChatStyleEnum.BOT;
                    break;
            }

            avatarColor = ChatBubbleUtilities.AVATAR_COLOR_CACHE.get(figure);
            username = userData.name;
        }

        switch (chatType) {
            case RoomSessionChatEvent.CHAT_TYPE_RESPECT:
                text = LocalizeText('widgets.chatbubble.respect', ['username'], [username]);

                if (GetConfigurationRecord('respect.options')['enabled']) PlaySound(GetConfigurationRecord('respect.options')['sound']);

                break;
            case RoomSessionChatEvent.CHAT_TYPE_PETREVIVE:
            case RoomSessionChatEvent.CHAT_TYPE_PET_REBREED_FERTILIZE:
            case RoomSessionChatEvent.CHAT_TYPE_PET_SPEED_FERTILIZE: {
                let textKey = 'widget.chatbubble.petrevived';

                if (chatType === RoomSessionChatEvent.CHAT_TYPE_PET_REBREED_FERTILIZE) {
                    textKey = 'widget.chatbubble.petrefertilized';
                } else if (chatType === RoomSessionChatEvent.CHAT_TYPE_PET_SPEED_FERTILIZE) {
                    textKey = 'widget.chatbubble.petspeedfertilized';
                }

                let targetUserName: string = null;

                const newRoomObject = GetRoomEngine().getRoomObject(roomSession.roomId, event.extraParam, RoomObjectCategory.UNIT);

                if (newRoomObject) {
                    const newUserData = roomSession.userDataManager.getUserDataByIndex(newRoomObject.id);

                    if (newUserData) targetUserName = newUserData.name;
                }

                text = LocalizeText(textKey, ['petName', 'userName'], [username, targetUserName]);
                break;
            }
            case RoomSessionChatEvent.CHAT_TYPE_PETRESPECT:
                text = LocalizeText('widget.chatbubble.petrespect', ['petname'], [username]);
                break;
            case RoomSessionChatEvent.CHAT_TYPE_PETTREAT:
                text = LocalizeText('widget.chatbubble.pettreat', ['petname'], [username]);
                break;
            case RoomSessionChatEvent.CHAT_TYPE_HAND_ITEM_RECEIVED:
                text = LocalizeText('widget.chatbubble.handitem', ['username', 'handitem'], [username, LocalizeText('handitem' + event.extraParam)]);
                break;
            case RoomSessionChatEvent.CHAT_TYPE_MUTE_REMAINING: {
                const remainingSeconds = Math.max(0, event.extraParam);
                const hours = Math.floor(remainingSeconds / 3600).toString();
                const minutes = Math.floor((remainingSeconds % 3600) / 60).toString();
                const seconds = (remainingSeconds % 60).toString();

                text = LocalizeText('widget.chatbubble.mutetime', ['hours', 'minutes', 'seconds'], [hours, minutes, seconds]);
                break;
            }
        }

        const isTranslatableChatType =
            chatType === RoomSessionChatEvent.CHAT_TYPE_SPEAK ||
            chatType === RoomSessionChatEvent.CHAT_TYPE_WHISPER ||
            chatType === RoomSessionChatEvent.CHAT_TYPE_SHOUT;
        const outgoingTranslation = isTranslatableChatType && userData.webID === ownUserId ? consumeOutgoingTranslation(text) : null;
        const originalText = outgoingTranslation?.originalText || text;
        const formattedText = RoomChatFormatter(originalText);
        const color = avatarColor && ('#' + avatarColor.toString(16).padStart(6, '0') || null);

        const chatMessage = new ChatBubbleMessage(
            userData.roomIndex,
            RoomObjectCategory.UNIT,
            roomSession.roomId,
            originalText,
            formattedText,
            username,
            { x: bubbleLocation.x, y: bubbleLocation.y },
            chatType,
            styleId,
            imageUrl,
            color
        );
        // The renderer adds bubbleWidthOverride to the chat event in Octane-Renderer#212; until that
        // lands the published event has no such field, so it is read as optional.
        chatMessage.textSize = getStoredChatTextSize();
        chatMessage.bubbleWidthOverride = (event as RoomSessionChatEvent & { bubbleWidthOverride?: number }).bubbleWidthOverride ?? -1;
        chatMessage.hideUsername = event.hideUsername === true;

        if (outgoingTranslation) {
            applyTranslationToBubble(
                chatMessage,
                outgoingTranslation.originalText,
                outgoingTranslation.translatedText,
                outgoingTranslation.detectedLanguage,
                outgoingTranslation.targetLanguage
            );
        }

        chatMessage.prefixText = event.prefixText || '';
        chatMessage.prefixColor = event.prefixColor || '';
        chatMessage.prefixIcon = event.prefixIcon || '';
        chatMessage.prefixEffect = event.prefixEffect || '';
        chatMessage.prefixFont = event.prefixFont || '';
        chatMessage.nickIcon = event.nickIcon || '';
        chatMessage.displayOrder = event.displayOrder || 'icon-prefix-name';

        setChatMessages((prevValue) => {
            const newValue = [...prevValue, chatMessage];

            if (newValue.length > CHAT_MESSAGES_MAX) newValue.shift();

            return newValue;
        });

        // Pet, Bot and Rentable Bot chat is fire-and-forget ("UDP-style"):
        // the live bubble already rendered above, but we deliberately skip
        // addChatEntry so the entry never lands in localStorage. A pet-heavy
        // room used to push 30+ KB per message (base64 head data URL) into
        // the chat history, exhausting the localStorage quota in minutes.
        // Real users still go through the full persisted path.
        const chatEntryId =
            userType === RoomObjectType.USER
                ? addChatEntry({
                      id: -1,
                      webId: userData.webID,
                      entityId: userData.roomIndex,
                      name: username,
                      look: userData.figure,
                      imageUrl,
                      textSize: chatMessage.textSize,
                      style: styleId,
                      chatType: chatType,
                      entityType: userData.type,
                      message: formattedText,
                      text: originalText,
                      timestamp: ChatHistoryCurrentDate(),
                      type: ChatEntryType.TYPE_CHAT,
                      roomId: roomSession.roomId,
                      color,
                      ...(outgoingTranslation
                          ? buildTranslatedEntryPatch(
                                outgoingTranslation.originalText,
                                outgoingTranslation.translatedText,
                                outgoingTranslation.detectedLanguage,
                                outgoingTranslation.targetLanguage
                            )
                          : {})
                  })
                : -1;

        if (imageRequest) {
            const bubbleId = chatMessage.id;

            void imageRequest
                .then((url) => {
                    if (!url || isDisposed.current) return;

                    setChatMessages((prevValue) => {
                        const index = prevValue.findIndex((chat) => chat.id === bubbleId);

                        if (index === -1) return prevValue;

                        // A new object, so the bubble view sees the change.
                        const bubble = Object.assign(Object.create(Object.getPrototypeOf(prevValue[index])), prevValue[index], { imageUrl: url });
                        const newValue = [...prevValue];

                        newValue[index] = bubble;

                        return newValue;
                    });

                    updateChatEntry(chatEntryId, { imageUrl: url });
                })
                .catch(() => undefined);
        }

        if (!settings.enabled || outgoingTranslation || !isTranslatableChatType || !text.trim().length) return;

        void translateIncoming(text).then((translation) => {
            if (!translation || isDisposed.current) return;

            applyAsyncTranslation(
                chatMessage.id,
                chatEntryId,
                translation.originalText,
                translation.translatedText,
                translation.detectedLanguage,
                translation.targetLanguage
            );
        });
    });

    useOctaneEvent<RoomDragEvent>(RoomDragEvent.ROOM_DRAG, (event) => {
        if (!chatMessages.length || event.roomId !== roomSession.roomId) return;

        const offsetX = event.offsetX;

        chatMessages.forEach((chat) => {
            if (!chat.elementRef) return;

            chat.left += offsetX;

            if (chat.location) chat.location = { ...chat.location, x: chat.location.x + offsetX };
        });
    });

    useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, (event) => {
        const parser = event.getParser();

        if (!parser.roomEnter) return;

        setChatSettings(parser.chat);
    });

    useMessageEvent<RoomChatSettingsEvent>(RoomChatSettingsEvent, (event) => {
        const parser = event.getParser();

        setChatSettings(parser.chat);
    });

    useEffect(() => {
        isDisposed.current = false;

        if (GetConfigurationValue<boolean>('chat.emoji.enabled', true)) loadEmojiShortcodes();

        return () => {
            isDisposed.current = true;
        };
    }, []);

    return { chatMessages, setChatMessages, chatSettings: effectiveChatSettings, getScrollSpeed, chatMode };
};

export const useChatWidget = useChatWidgetState;
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