Files
Nitro-V3/src/hooks/rooms/widgets/useChatInputState.ts
T
simoleo89 a4c9dd87db Split useChatInputWidget into state + actions (flat hooks layout)
Continues the proposal #4 split pattern (doorbell, poll, furni-chooser,
user-chooser, friend-request) for the chat-input widget. Splits the
334-line useChatInputWidget along the natural seam:

- useChatInputState — selectedUsername / floodBlocked / floodBlockedSeconds
  / isTyping / isIdle state plus the three event listeners
  (FLOOD_EVENT, ObjectSelected, ObjectDeselected) and the three lifecycle
  effects (flood-countdown, idle-auto-clear, typing-indicator sync).
- useChatInputActions — sendChat(text, chatType, recipientName, styleId).
  Carries the slash-command handler (":shake", ":rotate", ":zoom",
  ":screenshot", ":pickall", etc.) and the chat-vs-shout-vs-whisper
  dispatch path, with the optional outgoing-translation hook.
- useChatInputWidget — deprecated shim that composes both into the
  historical { selectedUsername, floodBlocked, floodBlockedSeconds,
  setIsTyping, setIsIdle, sendChat } shape so ChatInputView keeps
  working unchanged.

Bonus while in here:
- Guarded all roomSession reads in actions with optional chaining
  (the hook can be called during the brief no-room window between
  enter and leave).
- Dropped the useless 'if(isIdle)' inside the idle effect body — the
  early return guard above it already covers that branch.
2026-05-11 22:00:23 +02:00

114 lines
3.4 KiB
TypeScript

import { RoomEngineObjectEvent, RoomObjectCategory, RoomSessionChatEvent } from '@nitrots/nitro-renderer';
import { useEffect, useState } from 'react';
import { useNitroEvent } 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.
*/
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);
const { roomSession = null } = useRoom();
useNitroEvent<RoomSessionChatEvent>(RoomSessionChatEvent.FLOOD_EVENT, event =>
{
setFloodBlocked(true);
setFloodBlockedSeconds(parseFloat(event.message));
});
useObjectSelectedEvent(event =>
{
if(event.category !== RoomObjectCategory.UNIT) return;
const userData = roomSession?.userDataManager?.getUserDataByIndex(event.id);
if(!userData) return;
setSelectedUsername(userData.name);
});
useNitroEvent<RoomEngineObjectEvent>(RoomEngineObjectEvent.DESELECTED, () => setSelectedUsername(''));
useEffect(() =>
{
if(!floodBlocked) return;
let seconds = 0;
const interval = setInterval(() =>
{
setFloodBlockedSeconds(prevValue =>
{
seconds = ((prevValue || 0) - 1);
return seconds;
});
if(seconds < 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 };
};