5180 fichiers 38.5 Mo 0 téléchargements 304 vues mis à jour il y a 6 heures ui
nitro-v36-with-renderer-v36 / src / hooks / rooms / widgets / useWordQuizWidget.ts
useWordQuizWidget.ts 5 Ko Télécharger le fichier
import { AvatarAction, GetRoomEngine, IQuestion, RoomSessionWordQuizEvent } from '@octane/renderer';
import { useEffect, useRef, useState } from 'react';
import { VoteValue } from '../../../api';
import { useOctaneEvent } from '../../events';
import { useRoom } from '../useRoom';
import { usePollActions } from './usePollActions';

const DEFAULT_DISPLAY_DELAY = 4000;
const SIGN_FADE_DELAY = 3;

const useWordQuizWidgetState = () => {
    const [pollId, setPollId] = useState(-1);
    const [question, setQuestion] = useState<IQuestion>(null);
    const [answerSent, setAnswerSent] = useState(false);
    const [answerCounts, setAnswerCounts] = useState<Map<string, number>>(new Map());
    const [userAnswers, setUserAnswers] = useState<Map<number, VoteValue>>(new Map());
    // The question-clear timeout is a side-channel handle, not display
    // state — storing it in a ref avoids a re-render every time we
    // (re)schedule it and lets the cleanup effect read the *latest*
    // handle on unmount instead of the closed-over one.
    const questionClearTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
    const { answerPoll } = usePollActions();
    const { roomSession = null } = useRoom();

    const clearQuestion = () => {
        setPollId(-1);
        setQuestion(null);
    };

    const scheduleQuestionClear = (delay: number) => {
        if (questionClearTimeoutRef.current) clearTimeout(questionClearTimeoutRef.current);

        questionClearTimeoutRef.current = setTimeout(() => {
            questionClearTimeoutRef.current = null;
            clearQuestion();
        }, delay);
    };

    const vote = (vote: string) => {
        if (answerSent || !question) return;

        answerPoll(pollId, question.id, [vote]);

        setAnswerSent(true);
    };

    useOctaneEvent<RoomSessionWordQuizEvent>(RoomSessionWordQuizEvent.ANSWERED, (event) => {
        const userData = roomSession.userDataManager.getUserData(event.userId);

        if (!userData) return;

        setAnswerCounts(event.answerCounts);

        setUserAnswers((prevValue) => {
            // Bug fix: previously this read the closure-captured `userAnswers`
            // (which was stale by one render) instead of `prevValue`, so
            // rapid successive ANSWERED events for different users could
            // overwrite each other. Use prevValue.
            if (prevValue.has(userData.roomIndex)) return prevValue;

            const next = new Map(prevValue);
            next.set(userData.roomIndex, { value: event.value, secondsLeft: SIGN_FADE_DELAY });
            return next;
        });

        GetRoomEngine().updateRoomObjectUserGesture(
            roomSession.roomId,
            userData.roomIndex,
            AvatarAction.getGestureId(event.value === '0' ? AvatarAction.GESTURE_SAD : AvatarAction.GESTURE_SMILE)
        );
    });

    useOctaneEvent<RoomSessionWordQuizEvent>(RoomSessionWordQuizEvent.FINISHED, (event) => {
        if (question && question.id === event.questionId) {
            setAnswerCounts(event.answerCounts);
            setAnswerSent(true);

            scheduleQuestionClear(DEFAULT_DISPLAY_DELAY);
        }

        setUserAnswers(new Map());
    });

    useOctaneEvent<RoomSessionWordQuizEvent>(RoomSessionWordQuizEvent.QUESTION, (event) => {
        setPollId(event.id);
        setQuestion(event.question);
        setAnswerSent(false);
        setAnswerCounts(new Map());
        setUserAnswers(new Map());

        if (event.duration > 0) {
            const delay = event.duration < 1000 ? DEFAULT_DISPLAY_DELAY : event.duration;
            scheduleQuestionClear(delay);
        } else if (questionClearTimeoutRef.current) {
            clearTimeout(questionClearTimeoutRef.current);
            questionClearTimeoutRef.current = null;
        }
    });

    useEffect(() => {
        const tick = () => {
            setUserAnswers((prevValue) => {
                if (prevValue.size === 0) return prevValue;

                // Build new value objects + a new Map — don't decrement
                // secondsLeft on the objects still referenced by prevValue
                // (in-place mutation breaks memoization / StrictMode replay).
                const next = new Map(prevValue);

                next.forEach((value, key) => {
                    const secondsLeft = value.secondsLeft - 1;

                    if (secondsLeft <= 0) next.delete(key);
                    else next.set(key, { ...value, secondsLeft });
                });

                return next;
            });
        };

        const interval = setInterval(tick, 1000);

        return () => clearInterval(interval);
    }, []);

    useEffect(
        () => () => {
            if (questionClearTimeoutRef.current) {
                clearTimeout(questionClearTimeoutRef.current);
                questionClearTimeoutRef.current = null;
            }
        },
        []
    );

    return { question, answerSent, answerCounts, userAnswers, vote };
};

export const useWordQuizWidget = useWordQuizWidgetState;
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