mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
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.
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
import { AvatarExpressionEnum, CreateLinkEvent, GetEventDispatcher, GetRoomEngine, GetSessionDataManager, GetTicker, HabboClubLevelEnum, RoomControllerLevel, RoomRotatingEffect, RoomSettingsComposer, RoomShakingEffect, RoomZoomEvent, TextureUtils } from '@nitrots/nitro-renderer';
|
||||
import { useCallback } from 'react';
|
||||
import { ChatMessageTypeEnum, GetClubMemberLevel, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../../api';
|
||||
import { useNotification } from '../../notification';
|
||||
import { useTranslation } from '../../translation';
|
||||
import { useRoom } from '../useRoom';
|
||||
|
||||
/**
|
||||
* Pure imperative dispatch for the chat-input widget. Exposes
|
||||
* `sendChat(text, chatType, recipientName?, styleId?)` which:
|
||||
*
|
||||
* 1. Intercepts in-room slash commands (`:shake`, `:rotate`, `:zoom`,
|
||||
* `:screenshot`, `:pickall`, ...) and turns them into the matching
|
||||
* renderer/composer call — these never reach the server as chat
|
||||
* payload.
|
||||
* 2. Falls back to the regular default/shout/whisper composer path,
|
||||
* optionally piping the text through the translation pipeline if
|
||||
* outgoing translation is enabled.
|
||||
*
|
||||
* No state lives in this hook — the typing/flood/idle state belongs
|
||||
* to useChatInputState.
|
||||
*/
|
||||
export const useChatInputActions = () =>
|
||||
{
|
||||
const { showNitroAlert = null, showConfirm = null } = useNotification();
|
||||
const { settings, translateOutgoing, enqueueOutgoingTranslation } = useTranslation();
|
||||
const { roomSession = null } = useRoom();
|
||||
|
||||
const sendChat = useCallback((text: string, chatType: number, recipientName: string = '', styleId: number = 0) =>
|
||||
{
|
||||
if(text === '') return null;
|
||||
|
||||
const parts = text.split(' ');
|
||||
|
||||
if(parts.length > 0)
|
||||
{
|
||||
const firstPart = parts[0];
|
||||
let secondPart = '';
|
||||
|
||||
if(parts.length > 1) secondPart = parts[1];
|
||||
|
||||
if((firstPart.charAt(0) === ':') && (secondPart === 'x'))
|
||||
{
|
||||
const selectedAvatarId = GetRoomEngine().selectedAvatarId;
|
||||
|
||||
if(selectedAvatarId > -1)
|
||||
{
|
||||
const userData = roomSession?.userDataManager?.getUserDataByIndex(selectedAvatarId);
|
||||
|
||||
if(userData)
|
||||
{
|
||||
secondPart = userData.name;
|
||||
text = text.replace(' x', (' ' + userData.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch(firstPart.toLowerCase())
|
||||
{
|
||||
case ':shake':
|
||||
RoomShakingEffect.init(2500, 5000);
|
||||
RoomShakingEffect.turnVisualizationOn();
|
||||
|
||||
return null;
|
||||
|
||||
case ':rotate':
|
||||
RoomRotatingEffect.init(2500, 5000);
|
||||
RoomRotatingEffect.turnVisualizationOn();
|
||||
|
||||
return null;
|
||||
case ':d':
|
||||
case ';d':
|
||||
if(GetClubMemberLevel() === HabboClubLevelEnum.VIP)
|
||||
{
|
||||
roomSession?.sendExpressionMessage(AvatarExpressionEnum.LAUGH.ordinal);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'o/':
|
||||
case '_o/':
|
||||
roomSession?.sendExpressionMessage(AvatarExpressionEnum.WAVE.ordinal);
|
||||
|
||||
return null;
|
||||
case ':kiss':
|
||||
if(GetClubMemberLevel() === HabboClubLevelEnum.VIP)
|
||||
{
|
||||
roomSession?.sendExpressionMessage(AvatarExpressionEnum.BLOW.ordinal);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
break;
|
||||
case ':jump':
|
||||
if(GetClubMemberLevel() === HabboClubLevelEnum.VIP)
|
||||
{
|
||||
roomSession?.sendExpressionMessage(AvatarExpressionEnum.JUMP.ordinal);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
break;
|
||||
case ':idle':
|
||||
roomSession?.sendExpressionMessage(AvatarExpressionEnum.IDLE.ordinal);
|
||||
|
||||
return null;
|
||||
case '_b':
|
||||
roomSession?.sendExpressionMessage(AvatarExpressionEnum.RESPECT.ordinal);
|
||||
|
||||
return null;
|
||||
case ':sign':
|
||||
roomSession?.sendSignMessage(parseInt(secondPart));
|
||||
|
||||
return null;
|
||||
case ':iddqd':
|
||||
case ':flip':
|
||||
if(roomSession) GetEventDispatcher().dispatchEvent(new RoomZoomEvent(roomSession.roomId, -1, true));
|
||||
|
||||
return null;
|
||||
case ':zoom':
|
||||
if(roomSession) GetEventDispatcher().dispatchEvent(new RoomZoomEvent(roomSession.roomId, parseInt(secondPart)));
|
||||
|
||||
return null;
|
||||
case ':screenshot':
|
||||
if(!roomSession) return null;
|
||||
|
||||
{
|
||||
const texture = GetRoomEngine().createTextureFromRoom(roomSession.roomId, 1);
|
||||
|
||||
(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const imageUrl = await TextureUtils.generateImageUrl(texture);
|
||||
if(!imageUrl) return;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = imageUrl;
|
||||
link.download = `room_${ roomSession.roomId }_${ Date.now() }.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
console.warn('[Screenshot] Failed:', e);
|
||||
}
|
||||
})();
|
||||
}
|
||||
return null;
|
||||
case ':pickall':
|
||||
if(roomSession?.isRoomOwner || GetSessionDataManager().isModerator)
|
||||
{
|
||||
showConfirm(LocalizeText('room.confirm.pick_all'), () =>
|
||||
{
|
||||
GetSessionDataManager().sendSpecialCommandMessage(':pickall');
|
||||
},
|
||||
null, null, null, LocalizeText('generic.alert.title'));
|
||||
}
|
||||
|
||||
return null;
|
||||
case ':ejectall':
|
||||
if(roomSession?.isRoomOwner || GetSessionDataManager().isModerator || (roomSession?.controllerLevel ?? 0) >= RoomControllerLevel.GUEST)
|
||||
{
|
||||
showConfirm(LocalizeText('room.confirm.eject_all'), () =>
|
||||
{
|
||||
GetSessionDataManager().sendSpecialCommandMessage(':ejectall');
|
||||
},
|
||||
null, null, null, LocalizeText('generic.alert.title'));
|
||||
}
|
||||
return null;
|
||||
case ':furni':
|
||||
CreateLinkEvent('furni-chooser/');
|
||||
return null;
|
||||
case ':chooser':
|
||||
CreateLinkEvent('user-chooser/');
|
||||
return null;
|
||||
case ':floor':
|
||||
case ':bcfloor':
|
||||
if((roomSession?.controllerLevel ?? 0) >= RoomControllerLevel.ROOM_OWNER) CreateLinkEvent('floor-editor/show');
|
||||
|
||||
return null;
|
||||
case ':togglefps': {
|
||||
if(GetTicker().maxFPS > 0) GetTicker().maxFPS = 0;
|
||||
else GetTicker().maxFPS = GetConfigurationValue('system.animation.fps');
|
||||
|
||||
return null;
|
||||
}
|
||||
case ':client':
|
||||
case ':nitro':
|
||||
case ':billsonnn':
|
||||
showNitroAlert();
|
||||
return null;
|
||||
case ':settings':
|
||||
if(roomSession && (roomSession.isRoomOwner || GetSessionDataManager().isModerator))
|
||||
{
|
||||
SendMessageComposer(new RoomSettingsComposer(roomSession.roomId));
|
||||
}
|
||||
|
||||
return null;
|
||||
case ':customize':
|
||||
CreateLinkEvent('customize/show');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if(!roomSession) return null;
|
||||
|
||||
const preserveTrailingSpaces = (message: string) =>
|
||||
{
|
||||
if(message.startsWith(':')) return message;
|
||||
|
||||
return message.replace(/ +$/g, match => ' '.repeat(match.length));
|
||||
};
|
||||
|
||||
const dispatchChatMessage = (message: string) =>
|
||||
{
|
||||
const preservedMessage = preserveTrailingSpaces(message);
|
||||
|
||||
switch(chatType)
|
||||
{
|
||||
case ChatMessageTypeEnum.CHAT_DEFAULT:
|
||||
roomSession.sendChatMessage(preservedMessage, styleId);
|
||||
return;
|
||||
case ChatMessageTypeEnum.CHAT_SHOUT:
|
||||
roomSession.sendShoutMessage(preservedMessage, styleId);
|
||||
return;
|
||||
case ChatMessageTypeEnum.CHAT_WHISPER:
|
||||
roomSession.sendWhisperMessage(recipientName, preservedMessage, styleId);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const trimmedText = text.trimStart();
|
||||
const shouldTranslateOutgoing = settings.enabled && !!trimmedText.length && (trimmedText.charAt(0) !== ':');
|
||||
|
||||
if(!shouldTranslateOutgoing)
|
||||
{
|
||||
dispatchChatMessage(text);
|
||||
return null;
|
||||
}
|
||||
|
||||
void (async () =>
|
||||
{
|
||||
const translation = await translateOutgoing(text);
|
||||
|
||||
if(translation)
|
||||
{
|
||||
enqueueOutgoingTranslation(translation);
|
||||
dispatchChatMessage(translation.translatedText);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatchChatMessage(text);
|
||||
})();
|
||||
|
||||
return null;
|
||||
}, [ roomSession, settings, translateOutgoing, enqueueOutgoingTranslation, showConfirm, showNitroAlert ]);
|
||||
|
||||
return { sendChat };
|
||||
};
|
||||
@@ -0,0 +1,113 @@
|
||||
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 };
|
||||
};
|
||||
@@ -1,334 +1,17 @@
|
||||
import { AvatarExpressionEnum, CreateLinkEvent, GetEventDispatcher, GetRoomEngine, GetSessionDataManager, GetTicker, HabboClubLevelEnum, RoomControllerLevel, RoomEngineObjectEvent, RoomObjectCategory, RoomRotatingEffect, RoomSessionChatEvent, RoomSettingsComposer, RoomShakingEffect, RoomZoomEvent, TextureUtils } from '@nitrots/nitro-renderer';
|
||||
import { useEffect, useState } from 'react';
|
||||
import { ChatMessageTypeEnum, GetClubMemberLevel, GetConfigurationValue, LocalizeText, SendMessageComposer } from '../../../api';
|
||||
import { useNitroEvent } from '../../events';
|
||||
import { useNotification } from '../../notification';
|
||||
import { useTranslation } from '../../translation';
|
||||
import { useObjectSelectedEvent } from '../engine';
|
||||
import { useRoom } from '../useRoom';
|
||||
import { useChatInputActions } from './useChatInputActions';
|
||||
import { useChatInputState } from './useChatInputState';
|
||||
|
||||
const useChatInputWidgetState = () =>
|
||||
/**
|
||||
* @deprecated Use `useChatInputState` and `useChatInputActions`
|
||||
* directly. This shim preserves the
|
||||
* `{ selectedUsername, floodBlocked, floodBlockedSeconds, setIsTyping,
|
||||
* setIsIdle, sendChat }` shape so the single consumer (`ChatInputView`)
|
||||
* keeps working unchanged.
|
||||
*/
|
||||
export const useChatInputWidget = () =>
|
||||
{
|
||||
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 { showNitroAlert = null, showConfirm = null } = useNotification();
|
||||
const { settings, translateOutgoing, enqueueOutgoingTranslation } = useTranslation();
|
||||
const { roomSession = null } = useRoom();
|
||||
|
||||
const sendChat = (text: string, chatType: number, recipientName: string = '', styleId: number = 0) =>
|
||||
{
|
||||
if(text === '') return null;
|
||||
|
||||
const parts = text.split(' ');
|
||||
|
||||
if(parts.length > 0)
|
||||
{
|
||||
const firstPart = parts[0];
|
||||
let secondPart = '';
|
||||
|
||||
if(parts.length > 1) secondPart = parts[1];
|
||||
|
||||
if((firstPart.charAt(0) === ':') && (secondPart === 'x'))
|
||||
{
|
||||
const selectedAvatarId = GetRoomEngine().selectedAvatarId;
|
||||
|
||||
if(selectedAvatarId > -1)
|
||||
{
|
||||
const userData = roomSession.userDataManager.getUserDataByIndex(selectedAvatarId);
|
||||
|
||||
if(userData)
|
||||
{
|
||||
secondPart = userData.name;
|
||||
text = text.replace(' x', (' ' + userData.name));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
switch(firstPart.toLowerCase())
|
||||
{
|
||||
case ':shake':
|
||||
RoomShakingEffect.init(2500, 5000);
|
||||
RoomShakingEffect.turnVisualizationOn();
|
||||
|
||||
return null;
|
||||
|
||||
case ':rotate':
|
||||
RoomRotatingEffect.init(2500, 5000);
|
||||
RoomRotatingEffect.turnVisualizationOn();
|
||||
|
||||
return null;
|
||||
case ':d':
|
||||
case ';d':
|
||||
if(GetClubMemberLevel() === HabboClubLevelEnum.VIP)
|
||||
{
|
||||
roomSession.sendExpressionMessage(AvatarExpressionEnum.LAUGH.ordinal);
|
||||
}
|
||||
|
||||
break;
|
||||
case 'o/':
|
||||
case '_o/':
|
||||
roomSession.sendExpressionMessage(AvatarExpressionEnum.WAVE.ordinal);
|
||||
|
||||
return null;
|
||||
case ':kiss':
|
||||
if(GetClubMemberLevel() === HabboClubLevelEnum.VIP)
|
||||
{
|
||||
roomSession.sendExpressionMessage(AvatarExpressionEnum.BLOW.ordinal);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
break;
|
||||
case ':jump':
|
||||
if(GetClubMemberLevel() === HabboClubLevelEnum.VIP)
|
||||
{
|
||||
roomSession.sendExpressionMessage(AvatarExpressionEnum.JUMP.ordinal);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
break;
|
||||
case ':idle':
|
||||
roomSession.sendExpressionMessage(AvatarExpressionEnum.IDLE.ordinal);
|
||||
|
||||
return null;
|
||||
case '_b':
|
||||
roomSession.sendExpressionMessage(AvatarExpressionEnum.RESPECT.ordinal);
|
||||
|
||||
return null;
|
||||
case ':sign':
|
||||
roomSession.sendSignMessage(parseInt(secondPart));
|
||||
|
||||
return null;
|
||||
case ':iddqd':
|
||||
case ':flip':
|
||||
GetEventDispatcher().dispatchEvent(new RoomZoomEvent(roomSession.roomId, -1, true));
|
||||
|
||||
return null;
|
||||
case ':zoom':
|
||||
GetEventDispatcher().dispatchEvent(new RoomZoomEvent(roomSession.roomId, parseInt(secondPart)));
|
||||
|
||||
return null;
|
||||
case ':screenshot':
|
||||
const texture = GetRoomEngine().createTextureFromRoom(roomSession.roomId, 1);
|
||||
|
||||
(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
const imageUrl = await TextureUtils.generateImageUrl(texture);
|
||||
if (!imageUrl) return;
|
||||
|
||||
const link = document.createElement('a');
|
||||
link.href = imageUrl;
|
||||
link.download = `room_${ roomSession.roomId }_${ Date.now() }.png`;
|
||||
document.body.appendChild(link);
|
||||
link.click();
|
||||
document.body.removeChild(link);
|
||||
}
|
||||
catch (e)
|
||||
{
|
||||
console.warn('[Screenshot] Failed:', e);
|
||||
}
|
||||
})();
|
||||
return null;
|
||||
case ':pickall':
|
||||
if(roomSession.isRoomOwner || GetSessionDataManager().isModerator)
|
||||
{
|
||||
showConfirm(LocalizeText('room.confirm.pick_all'), () =>
|
||||
{
|
||||
GetSessionDataManager().sendSpecialCommandMessage(':pickall');
|
||||
},
|
||||
null, null, null, LocalizeText('generic.alert.title'));
|
||||
}
|
||||
|
||||
return null;
|
||||
case ':ejectall':
|
||||
if(roomSession.isRoomOwner || GetSessionDataManager().isModerator || roomSession.controllerLevel >= RoomControllerLevel.GUEST)
|
||||
{
|
||||
showConfirm(LocalizeText('room.confirm.eject_all'), () =>
|
||||
{
|
||||
GetSessionDataManager().sendSpecialCommandMessage(':ejectall');
|
||||
},
|
||||
null, null, null, LocalizeText('generic.alert.title'));
|
||||
}
|
||||
return null;
|
||||
case ':furni':
|
||||
CreateLinkEvent('furni-chooser/');
|
||||
return null;
|
||||
case ':chooser':
|
||||
CreateLinkEvent('user-chooser/');
|
||||
return null;
|
||||
case ':floor':
|
||||
case ':bcfloor':
|
||||
if(roomSession.controllerLevel >= RoomControllerLevel.ROOM_OWNER) CreateLinkEvent('floor-editor/show');
|
||||
|
||||
return null;
|
||||
case ':togglefps': {
|
||||
if(GetTicker().maxFPS > 0) GetTicker().maxFPS = 0;
|
||||
else GetTicker().maxFPS = GetConfigurationValue('system.animation.fps');
|
||||
|
||||
return null;
|
||||
}
|
||||
case ':client':
|
||||
case ':nitro':
|
||||
case ':billsonnn':
|
||||
showNitroAlert();
|
||||
return null;
|
||||
case ':settings':
|
||||
if(roomSession.isRoomOwner || GetSessionDataManager().isModerator)
|
||||
{
|
||||
SendMessageComposer(new RoomSettingsComposer(roomSession.roomId));
|
||||
}
|
||||
|
||||
return null;
|
||||
case ':customize':
|
||||
CreateLinkEvent('customize/show');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
const preserveTrailingSpaces = (message: string) =>
|
||||
{
|
||||
if(message.startsWith(':')) return message;
|
||||
|
||||
return message.replace(/ +$/g, match => '\u00A0'.repeat(match.length));
|
||||
};
|
||||
|
||||
const dispatchChatMessage = (message: string) =>
|
||||
{
|
||||
const preservedMessage = preserveTrailingSpaces(message);
|
||||
|
||||
switch(chatType)
|
||||
{
|
||||
case ChatMessageTypeEnum.CHAT_DEFAULT:
|
||||
roomSession.sendChatMessage(preservedMessage, styleId);
|
||||
return;
|
||||
case ChatMessageTypeEnum.CHAT_SHOUT:
|
||||
roomSession.sendShoutMessage(preservedMessage, styleId);
|
||||
return;
|
||||
case ChatMessageTypeEnum.CHAT_WHISPER:
|
||||
roomSession.sendWhisperMessage(recipientName, preservedMessage, styleId);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
const trimmedText = text.trimStart();
|
||||
const shouldTranslateOutgoing = settings.enabled && !!trimmedText.length && (trimmedText.charAt(0) !== ':');
|
||||
|
||||
if(!shouldTranslateOutgoing)
|
||||
{
|
||||
dispatchChatMessage(text);
|
||||
return null;
|
||||
}
|
||||
|
||||
void (async () =>
|
||||
{
|
||||
const translation = await translateOutgoing(text);
|
||||
|
||||
if(translation)
|
||||
{
|
||||
enqueueOutgoingTranslation(translation);
|
||||
dispatchChatMessage(translation.translatedText);
|
||||
return;
|
||||
}
|
||||
|
||||
dispatchChatMessage(text);
|
||||
})();
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
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, event => 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;
|
||||
|
||||
let timeout: ReturnType<typeof setTimeout> = null;
|
||||
|
||||
if(isIdle)
|
||||
{
|
||||
timeout = setTimeout(() =>
|
||||
{
|
||||
setIsIdle(false);
|
||||
setIsTyping(false);
|
||||
}, 10000);
|
||||
}
|
||||
|
||||
return () => clearTimeout(timeout);
|
||||
}, [ isIdle ]);
|
||||
|
||||
useEffect(() =>
|
||||
{
|
||||
if(isTyping)
|
||||
{
|
||||
if(!typingStartedSent)
|
||||
{
|
||||
setTypingStartedSent(true);
|
||||
|
||||
roomSession.sendChatTypingMessage(isTyping);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
if(typingStartedSent)
|
||||
{
|
||||
setTypingStartedSent(false);
|
||||
|
||||
roomSession.sendChatTypingMessage(isTyping);
|
||||
}
|
||||
}
|
||||
}, [ roomSession, isTyping, typingStartedSent ]);
|
||||
const { selectedUsername, floodBlocked, floodBlockedSeconds, setIsTyping, setIsIdle } = useChatInputState();
|
||||
const { sendChat } = useChatInputActions();
|
||||
|
||||
return { selectedUsername, floodBlocked, floodBlockedSeconds, setIsTyping, setIsIdle, sendChat };
|
||||
};
|
||||
|
||||
export const useChatInputWidget = useChatInputWidgetState;
|
||||
|
||||
Reference in New Issue
Block a user