feat(navigator): wired-tools-style hook split (Store + 3 filters)

Splits the 492-line useNavigator god-hook into a useBetween-backed
useNavigatorStore closure plus three flat-shape filters
(useNavigatorData, useNavigatorUiState, useNavigatorActions), mirroring
the wired-tools layout. sendSearch + reloadCurrentSearch are extracted
as named actions out of NavigatorView locals.

Door-mode handling is removed from this store and lives in useDoorState
(committed previously) - see GetGuestRoomResultEvent and
GenericErrorEvent dual-subscription with mutually exclusive filters.

The simpleAlert dependency is lifted out of the useBetween scope via a
module-level _simpleAlert ref + _injectSimpleAlert() to avoid nested
useBetween calls that corrupt use-between's module-level dispatcher
state. The ref is null in tests (no events fire during smoke tests) and
is populated in production by the navigator consumer before any alert
is needed.

The barrel index.ts no longer re-exports useNavigator. The 13 consumers
will fail typecheck until the next commit migrates them; the hook files
themselves are clean. Smoke test covers filter shapes.

INTENTIONAL INTERMEDIATE-BROKEN COMMIT: yarn typecheck is RED at this
SHA on the 13 consumer files. The next commit (consumer migration sweep)
brings it back to green.
This commit is contained in:
simoleo89
2026-05-27 18:44:24 +02:00
parent fac2878bc8
commit 8ab0021af6
7 changed files with 483 additions and 1 deletions
+354
View File
@@ -0,0 +1,354 @@
import { CanCreateRoomEventEvent, CantConnectMessageParser, CreateLinkEvent,
FavouriteChangedEvent, FavouritesEvent, FlatCreatedEvent,
FollowFriendMessageComposer, GenericErrorEvent, GetGuestRoomMessageComposer,
GetGuestRoomResultEvent, GetRoomSessionManager, GetSessionDataManager,
GetUserEventCatsMessageComposer, GetUserFlatCatsMessageComposer,
HabboWebTools, LegacyExternalInterface, NavigatorCategoryDataParser,
NavigatorEventCategoryDataParser, NavigatorHomeRoomEvent,
NavigatorMetadataEvent, NavigatorOpenRoomCreatorEvent, NavigatorSavedSearch,
NavigatorSearchComposer, NavigatorSearchesEvent, NavigatorSearchEvent,
NavigatorSearchResultSet, NavigatorTopLevelContext, NitroEventType,
RoomDataParser, RoomEnterErrorEvent, RoomEntryInfoMessageEvent,
RoomForwardEvent, RoomScoreEvent, RoomSettingsUpdatedEvent,
SecurityLevel, UserEventCatsEvent, UserFlatCatsEvent,
UserInfoEvent, UserPermissionsEvent } from '@nitrots/nitro-renderer';
import { useCallback, useRef, useState } from 'react';
import { CreateRoomSession, GetConfigurationValue, INavigatorData,
LocalizeText, NotificationAlertType, SendMessageComposer,
TryVisitRoom, VisitDesktop } from '../../api';
import { useMessageEvent, useNitroEvent } from '../events';
import { useNavigatorUiStore } from './navigatorUiStore';
// Module-level reference to simpleAlert, injected by useNavigatorActions
// (which runs in a real React dispatcher context, outside useBetween).
// Avoids nested useBetween calls that corrupt use-between's module-level state.
type SimpleAlertFn = (message: string, type?: string, clickUrl?: string, clickUrlText?: string, title?: string, imageUrl?: string) => void;
let _simpleAlert: SimpleAlertFn | null = null;
export const _injectSimpleAlert = (fn: SimpleAlertFn | null) => { _simpleAlert = fn; };
export const useNavigatorStore = () =>
{
const [ categories, setCategories ] = useState<NavigatorCategoryDataParser[]>(null);
const [ eventCategories, setEventCategories ] = useState<NavigatorEventCategoryDataParser[]>(null);
const [ favouriteRoomIds, setFavouriteRoomIds ] = useState<number[]>([]);
const [ topLevelContext, setTopLevelContext ] = useState<NavigatorTopLevelContext>(null);
const [ topLevelContexts, setTopLevelContexts ] = useState<NavigatorTopLevelContext[]>(null);
const [ searchResult, setSearchResult ] = useState<NavigatorSearchResultSet>(null);
const [ navigatorSearches, setNavigatorSearches ] = useState<NavigatorSavedSearch[]>(null);
const [ navigatorData, setNavigatorData ] = useState<INavigatorData>({
settingsReceived: false,
homeRoomId: 0,
enteredGuestRoom: null,
currentRoomOwner: false,
currentRoomId: 0,
currentRoomIsStaffPick: false,
createdFlatId: 0,
avatarId: 0,
roomPicker: false,
eventMod: false,
currentRoomRating: 0,
canRate: true
});
// Refs let handlers stay [] deps without losing access to fresh state.
const topLevelContextsRef = useRef(topLevelContexts);
topLevelContextsRef.current = topLevelContexts;
const topLevelContextRef = useRef(topLevelContext);
topLevelContextRef.current = topLevelContext;
const searchResultRef = useRef(searchResult);
searchResultRef.current = searchResult;
const simpleAlert = _simpleAlert;
const sendSearch = useCallback((searchValue: string, contextCode: string) =>
{
useNavigatorUiStore.getState().closeCreator();
SendMessageComposer(new NavigatorSearchComposer(contextCode, searchValue));
useNavigatorUiStore.getState().setLoading(true);
}, []);
const reloadCurrentSearch = useCallback(() =>
{
if(!useNavigatorUiStore.getState().isReady)
{
useNavigatorUiStore.getState().requestSearch();
return;
}
const sr = searchResultRef.current;
if(sr)
{
sendSearch(sr.data, sr.code);
return;
}
const ctx = topLevelContextRef.current;
if(!ctx) return;
sendSearch('', ctx.code);
}, [ sendSearch ]);
useMessageEvent<FavouritesEvent>(FavouritesEvent, useCallback(event =>
{
const parser = event.getParser();
const favoriteIds = (parser.favoriteRoomIds || []).map((x: any) => Number(x));
setFavouriteRoomIds(favoriteIds);
}, []));
useMessageEvent<FavouriteChangedEvent>(FavouriteChangedEvent, useCallback(event =>
{
const parser = event.getParser();
const roomId = Number(parser.flatId);
const added = !!parser.added;
setFavouriteRoomIds(prev =>
{
const ids = (prev || []).map((x: any) => Number(x));
if(added) return ids.includes(roomId) ? ids : [ ...ids, roomId ];
return ids.filter(id => id !== roomId);
});
}, []));
useMessageEvent<RoomSettingsUpdatedEvent>(RoomSettingsUpdatedEvent, useCallback(event =>
{
const parser = event.getParser();
SendMessageComposer(new GetGuestRoomMessageComposer(parser.roomId, false, false));
}, []));
useMessageEvent<CanCreateRoomEventEvent>(CanCreateRoomEventEvent, useCallback(event =>
{
const parser = event.getParser();
if(parser.canCreate) return;
simpleAlert(LocalizeText(`navigator.cannotcreateevent.error.${ parser.errorCode }`), null, null, null, LocalizeText('navigator.cannotcreateevent.title'));
}, [ simpleAlert ]));
useMessageEvent<UserInfoEvent>(UserInfoEvent, useCallback(event =>
{
SendMessageComposer(new GetUserFlatCatsMessageComposer());
SendMessageComposer(new GetUserEventCatsMessageComposer());
}, []));
useMessageEvent<UserPermissionsEvent>(UserPermissionsEvent, useCallback(event =>
{
const parser = event.getParser();
setNavigatorData(prev => ({
...prev,
eventMod: parser.securityLevel >= SecurityLevel.MODERATOR,
roomPicker: parser.securityLevel >= SecurityLevel.COMMUNITY
}));
}, []));
useMessageEvent<RoomForwardEvent>(RoomForwardEvent, useCallback(event =>
{
const parser = event.getParser();
TryVisitRoom(parser.roomId);
}, []));
useMessageEvent<RoomEntryInfoMessageEvent>(RoomEntryInfoMessageEvent, useCallback(event =>
{
const parser = event.getParser();
setNavigatorData(prev => ({
...prev,
enteredGuestRoom: null,
currentRoomOwner: parser.isOwner,
currentRoomId: parser.roomId
}));
SendMessageComposer(new GetGuestRoomMessageComposer(parser.roomId, true, false));
if(LegacyExternalInterface.available) LegacyExternalInterface.call('legacyTrack', 'navigator', 'private', [ parser.roomId ]);
}, []));
useMessageEvent<GetGuestRoomResultEvent>(GetGuestRoomResultEvent, useCallback(event =>
{
const parser = event.getParser();
if(parser.roomEnter)
{
setNavigatorData(prev =>
{
const next = { ...prev };
next.enteredGuestRoom = parser.data;
next.currentRoomIsStaffPick = parser.staffPick;
const isCreated = next.createdFlatId === parser.data.roomId;
if(!isCreated && parser.data.displayRoomEntryAd)
{
if(GetConfigurationValue<boolean>('roomenterad.habblet.enabled', false)) HabboWebTools.openRoomEnterAd();
}
next.createdFlatId = 0;
return next;
});
return;
}
if(parser.roomForward)
{
// Door-mode branches (DOORBELL_STATE / PASSWORD_STATE) are handled by useDoorState — skip them here.
const isOwner = parser.data.ownerName === GetSessionDataManager().userName;
if(!isOwner && !parser.isGroupMember)
{
if(parser.data.doorMode === RoomDataParser.DOORBELL_STATE) return;
if(parser.data.doorMode === RoomDataParser.PASSWORD_STATE) return;
}
if((parser.data.doorMode === RoomDataParser.NOOB_STATE) && !GetSessionDataManager().isAmbassador && !GetSessionDataManager().isRealNoob && !GetSessionDataManager().isModerator) return;
CreateRoomSession(parser.data.roomId);
return;
}
setNavigatorData(prev => ({
...prev,
enteredGuestRoom: parser.data,
currentRoomIsStaffPick: parser.staffPick
}));
}, []));
useMessageEvent<RoomScoreEvent>(RoomScoreEvent, useCallback(event =>
{
const parser = event.getParser();
setNavigatorData(prev => ({
...prev,
currentRoomRating: parser.totalLikes,
canRate: parser.canLike
}));
}, []));
useMessageEvent<GenericErrorEvent>(GenericErrorEvent, useCallback(event =>
{
const parser = event.getParser();
// -100002 (wrong password) is handled by useDoorState — skip it here.
switch(parser.errorCode)
{
case 4009:
simpleAlert(LocalizeText('navigator.alert.need.to.be.vip'), NotificationAlertType.DEFAULT, null, null, LocalizeText('generic.alert.title'));
return;
case 4010:
simpleAlert(LocalizeText('navigator.alert.invalid_room_name'), NotificationAlertType.DEFAULT, null, null, LocalizeText('generic.alert.title'));
return;
case 4011:
simpleAlert(LocalizeText('navigator.alert.cannot_perm_ban'), NotificationAlertType.DEFAULT, null, null, LocalizeText('generic.alert.title'));
return;
case 4013:
simpleAlert(LocalizeText('navigator.alert.room_in_maintenance'), NotificationAlertType.DEFAULT, null, null, LocalizeText('generic.alert.title'));
return;
}
}, [ simpleAlert ]));
useMessageEvent<NavigatorMetadataEvent>(NavigatorMetadataEvent, useCallback(event =>
{
const parser = event.getParser();
setTopLevelContexts(parser.topLevelContexts);
setTopLevelContext(parser.topLevelContexts.length ? parser.topLevelContexts[0] : null);
}, []));
useMessageEvent<NavigatorSearchEvent>(NavigatorSearchEvent, useCallback(event =>
{
const parser = event.getParser();
const contexts = topLevelContextsRef.current;
setTopLevelContext(prev =>
{
let next = prev;
if(!next) next = (contexts && contexts.length && contexts[0]) || null;
if(!next) return null;
if(contexts && contexts.length)
{
for(const ctx of contexts)
{
if(ctx.code === parser.result.code) next = ctx;
}
}
return next;
});
setSearchResult(parser.result);
useNavigatorUiStore.getState().setLoading(false);
}, []));
useMessageEvent<UserFlatCatsEvent>(UserFlatCatsEvent, useCallback(event =>
{
const parser = event.getParser();
setCategories(parser.categories);
}, []));
useMessageEvent<UserEventCatsEvent>(UserEventCatsEvent, useCallback(event =>
{
const parser = event.getParser();
setEventCategories(parser.categories);
}, []));
useMessageEvent<FlatCreatedEvent>(FlatCreatedEvent, useCallback(event =>
{
const parser = event.getParser();
CreateRoomSession(parser.roomId);
}, []));
useNitroEvent(NitroEventType.SOCKET_RECONNECTING, useCallback(() =>
{
setNavigatorData(prev => ({ ...prev, settingsReceived: false }));
}, []));
useMessageEvent<NavigatorHomeRoomEvent>(NavigatorHomeRoomEvent, useCallback(event =>
{
const parser = event.getParser();
let prevSettingsReceived = false;
setNavigatorData(prev =>
{
prevSettingsReceived = prev.settingsReceived;
return { ...prev, homeRoomId: parser.homeRoomId, settingsReceived: true };
});
if(prevSettingsReceived) return;
if(GetRoomSessionManager().viewerSession) return;
let forwardType = -1;
let forwardId = -1;
if((GetConfigurationValue<string>('friend.id') !== undefined) && (parseInt(GetConfigurationValue<string>('friend.id')) > 0))
{
forwardType = 0;
SendMessageComposer(new FollowFriendMessageComposer(parseInt(GetConfigurationValue<string>('friend.id'))));
}
if((GetConfigurationValue<number>('forward.type') !== undefined) && (GetConfigurationValue<number>('forward.id') !== undefined))
{
forwardType = parseInt(GetConfigurationValue<string>('forward.type'));
forwardId = parseInt(GetConfigurationValue<string>('forward.id'));
}
if(forwardType === 2)
{
TryVisitRoom(forwardId);
}
else if((forwardType === -1) && (parser.roomIdToEnter > 0))
{
CreateLinkEvent('navigator/close');
CreateRoomSession(parser.roomIdToEnter !== parser.homeRoomId ? parser.roomIdToEnter : parser.homeRoomId);
}
}, []));
useMessageEvent<RoomEnterErrorEvent>(RoomEnterErrorEvent, useCallback(event =>
{
const parser = event.getParser();
switch(parser.reason)
{
case CantConnectMessageParser.REASON_FULL:
simpleAlert(LocalizeText('navigator.guestroomfull.text'), NotificationAlertType.DEFAULT, null, null, LocalizeText('navigator.guestroomfull.title'));
break;
case CantConnectMessageParser.REASON_QUEUE_ERROR:
simpleAlert(LocalizeText(`room.queue.error.${ parser.parameter }`), NotificationAlertType.DEFAULT, null, null, LocalizeText('room.queue.error.title'));
break;
case CantConnectMessageParser.REASON_BANNED:
simpleAlert(LocalizeText('navigator.banned.text'), NotificationAlertType.DEFAULT, null, null, LocalizeText('navigator.banned.title'));
break;
default:
simpleAlert(LocalizeText('room.queue.error.title'), NotificationAlertType.DEFAULT, null, null, LocalizeText('room.queue.error.title'));
break;
}
if(GetRoomSessionManager().isReconnecting) return;
VisitDesktop();
}, [ simpleAlert ]));
useMessageEvent<NavigatorOpenRoomCreatorEvent>(NavigatorOpenRoomCreatorEvent, useCallback(_event =>
{
CreateLinkEvent('navigator/show');
}, []));
useMessageEvent<NavigatorSearchesEvent>(NavigatorSearchesEvent, useCallback(event =>
{
const parser = event.getParser();
if(!parser) return;
setNavigatorSearches(parser.searches);
}, []));
return {
categories, eventCategories, favouriteRoomIds,
topLevelContext, topLevelContexts,
searchResult, navigatorSearches, navigatorData,
sendSearch, reloadCurrentSearch
};
};