useChatInputState.ts 3.9 Ko
Télécharger le fichier
import { RoomEngineObjectEvent, RoomObjectCategory, RoomSessionChatEvent } from '@octane/renderer';
import { useEffect, useRef, useState } from 'react';
import { useOctaneEvent } from '../../events';
import { useObjectSelectedEvent } from '../engine';
import { useRoom } from '../useRoom';
/**
* State + event subscriptions for the chat-input widget. Pure
* imperative dispatch (sendChat) lives in useChatInputActions.
*
* - selectedUsername → tracks the last avatar the user clicked,
* used by `/whisper` shortcuts.
* - floodBlocked / → flood-throttle banner state, driven by the
* floodBlockedSeconds renderer's FLOOD_EVENT plus a 1s tick.
* - isTyping / → typing indicator + 10s idle auto-clear, with
* isIdle an internal `typingStartedSent` ref so the
* outgoing sendChatTypingMessage only fires on
* state edges (start / stop), not every render.
*/
/** The seconds of a flood block the server sent, rounded up; 0 for a message that is not a positive number. */
export const floodBlockSeconds = (message: string): number => {
const seconds = Number.parseFloat(message);
return Number.isFinite(seconds) && seconds > 0 ? Math.ceil(seconds) : 0;
};
export const useChatInputState = () => {
const [selectedUsername, setSelectedUsername] = useState('');
const [isTyping, setIsTyping] = useState<boolean>(false);
const [typingStartedSent, setTypingStartedSent] = useState(false);
const [isIdle, setIsIdle] = useState(false);
const [floodBlocked, setFloodBlocked] = useState(false);
const [floodBlockedSeconds, setFloodBlockedSeconds] = useState(0);
// The countdown itself: it was worked out inside a state updater and read right after, which React does not
// promise to have run, and a message that was not a number (NaN) ended the block at once.
const floodRemainingRef = useRef(0);
const { roomSession = null } = useRoom();
useOctaneEvent<RoomSessionChatEvent>(RoomSessionChatEvent.FLOOD_EVENT, (event) => {
const seconds = floodBlockSeconds(event.message);
if (seconds <= 0) return;
floodRemainingRef.current = seconds;
setFloodBlockedSeconds(seconds);
setFloodBlocked(true);
});
useObjectSelectedEvent((event) => {
if (event.category !== RoomObjectCategory.UNIT) return;
const userData = roomSession?.userDataManager?.getUserDataByIndex(event.id);
if (!userData) return;
setSelectedUsername(userData.name);
});
useOctaneEvent<RoomEngineObjectEvent>(RoomEngineObjectEvent.DESELECTED, () => setSelectedUsername(''));
useEffect(() => {
if (!floodBlocked) return;
const interval = setInterval(() => {
floodRemainingRef.current -= 1;
setFloodBlockedSeconds(Math.max(0, floodRemainingRef.current));
if (floodRemainingRef.current <= 0) {
clearInterval(interval);
setFloodBlocked(false);
}
}, 1000);
return () => clearInterval(interval);
}, [floodBlocked]);
useEffect(() => {
if (!isIdle) return;
const timeout = setTimeout(() => {
setIsIdle(false);
setIsTyping(false);
}, 10000);
return () => clearTimeout(timeout);
}, [isIdle]);
useEffect(() => {
if (!roomSession) return;
if (isTyping) {
if (!typingStartedSent) {
setTypingStartedSent(true);
roomSession.sendChatTypingMessage(isTyping);
}
} else {
if (typingStartedSent) {
setTypingStartedSent(false);
roomSession.sendChatTypingMessage(isTyping);
}
}
}, [roomSession, isTyping, typingStartedSent]);
return { selectedUsername, floodBlocked, floodBlockedSeconds, setIsTyping, setIsIdle };
};
