Split useUserChooserWidget into state + actions (flat hooks layout)

Speculare di useFurniChooserWidget — stesso split + stesso layout (flat
in src/hooks/rooms/widgets/). User chooser è il gemello del furni
chooser nella shape: items list popolata da room scan + due bridge
events (added/removed) + selectItem imperativo.

Split
- src/hooks/rooms/widgets/useUserChooserState.ts (new):
    items + onClose + populateChooser + useUserAddedEvent +
    useUserRemovedEvent. Helper buildUserItem dedupa la costruzione
    di RoomObjectItem fra populateChooser e l'add handler (~20
    righe di duplicazione in meno).
    Plus: aggiunto ?. su roomSession e userDataManager (lo stesso
    bug pattern del PetTrainingPanel fixato altrove).
- src/hooks/rooms/widgets/useUserChooserActions.ts (new):
    selectItem puro.
- src/hooks/rooms/widgets/useUserChooserWidget.ts: kept as a
    deprecated shim that composes both and preserves
    { items, onClose, selectItem, populateChooser } per il consumer
    UserChooserWidgetView.

Verifica
- yarn eslint sui 4 file toccati: 0 errors / 0 warnings.
- yarn test: 49/49 passing.
- yarn tsc: clean.

Sequenza god-hook split adesso a 4 (doorbell, poll, furni-chooser,
user-chooser). Rimangono: useFriendRequestWidget, usePetPackageWidget,
useWordQuizWidget, useChatInputWidget, useChatWidget,
useAvatarInfoWidget, useFilterWordsWidget.
This commit is contained in:
simoleo89
2026-05-11 17:45:31 +00:00
parent 0ae371ee09
commit 85fc82794d
4 changed files with 121 additions and 92 deletions
+2
View File
@@ -15,5 +15,7 @@ export * from './usePetPackageWidget';
export * from './usePollActions';
export * from './usePollSubscriptions';
export * from './usePollWidget';
export * from './useUserChooserActions';
export * from './useUserChooserState';
export * from './useUserChooserWidget';
export * from './useWordQuizWidget';
@@ -0,0 +1,16 @@
import { GetRoomEngine } from '@nitrots/nitro-renderer';
import { GetRoomSession, RoomObjectItem } from '../../../api';
/**
* Imperative actions for the User chooser. Stateless — split from
* useUserChooserState so components that only need to dispatch a
* selection don't subscribe to the user-add/remove lifecycle events.
*/
export const useUserChooserActions = () => ({
selectItem: (item: RoomObjectItem): void =>
{
if(!item) return;
GetRoomEngine().selectRoomObject(GetRoomSession().roomId, item.id, item.category);
}
});
@@ -0,0 +1,92 @@
import { GetRoomEngine, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { useState } from 'react';
import { GetRoomSession, RoomObjectItem } from '../../../api';
import { useUserAddedEvent, useUserRemovedEvent } from '../engine';
const resolveUserType = (userType: number): string =>
{
switch(userType)
{
case 1: return 'Habbo';
case 2: return 'Pet';
case 3: return 'Bot';
default: return '-';
}
};
const buildUserItem = (roomIndex: number): RoomObjectItem | null =>
{
if(roomIndex < 0) return null;
const userData = GetRoomSession()?.userDataManager?.getUserDataByIndex(roomIndex);
if(!userData) return null;
if(userData.type !== 1) return null;
return new RoomObjectItem(userData.roomIndex, RoomObjectCategory.UNIT, userData.name, 0, '-', resolveUserType(userData.type));
};
/**
* State + event subscriptions for the User chooser widget. Pure
* imperative actions (selectItem) live in useUserChooserActions.
*/
export const useUserChooserState = () =>
{
const [ items, setItems ] = useState<RoomObjectItem[]>(null);
const onClose = () => setItems(null);
const populateChooser = () =>
{
const session = GetRoomSession();
if(!session) return;
const roomObjects = GetRoomEngine().getRoomObjects(session.roomId, RoomObjectCategory.UNIT);
setItems(roomObjects
.map(roomObject => buildUserItem(roomObject.id))
.filter((item): item is RoomObjectItem => item !== null)
.sort((a, b) => ((a.name < b.name) ? -1 : 1)));
};
useUserAddedEvent(!!items, event =>
{
const item = buildUserItem(event.id);
if(!item) return;
setItems(prevValue =>
{
const newValue = [ ...(prevValue ?? []), item ];
newValue.sort((a, b) => ((a.name < b.name) ? -1 : 1));
return newValue;
});
});
useUserRemovedEvent(!!items, event =>
{
if(event.id < 0) return;
setItems(prevValue =>
{
if(!prevValue) return prevValue;
const newValue = [ ...prevValue ];
for(let i = 0; i < newValue.length; i++)
{
const existingValue = newValue[i];
if((existingValue.id !== event.id) || (existingValue.category !== event.category)) continue;
newValue.splice(i, 1);
break;
}
return newValue;
});
});
return { items, onClose, populateChooser };
};
+11 -92
View File
@@ -1,97 +1,16 @@
import { GetRoomEngine, RoomObjectCategory } from '@nitrots/nitro-renderer';
import { useState } from 'react';
import { GetRoomSession, RoomObjectItem } from '../../../api';
import { useUserAddedEvent, useUserRemovedEvent } from '../engine';
import { useRoom } from '../useRoom';
import { useUserChooserActions } from './useUserChooserActions';
import { useUserChooserState } from './useUserChooserState';
const resolveUserType = (userType: number): string =>
/**
* @deprecated Use `useUserChooserState` (data + close + populate) and
* `useUserChooserActions` (imperative selectItem) directly. This shim
* preserves the `{ items, onClose, selectItem, populateChooser }` shape
* for existing consumers.
*/
export const useUserChooserWidget = () =>
{
switch(userType)
{
case 1: return 'Habbo';
case 2: return 'Pet';
case 3: return 'Bot';
default: return '-';
}
};
const useUserChooserWidgetState = () =>
{
const [ items, setItems ] = useState<RoomObjectItem[]>(null);
const { roomSession = null } = useRoom();
const onClose = () => setItems(null);
const selectItem = (item: RoomObjectItem) => item && GetRoomEngine().selectRoomObject(GetRoomSession().roomId, item.id, item.category);
const populateChooser = () =>
{
const roomSession = GetRoomSession();
const roomObjects = GetRoomEngine().getRoomObjects(roomSession.roomId, RoomObjectCategory.UNIT);
setItems(roomObjects
.map(roomObject =>
{
if(roomObject.id < 0) return null;
const userData = roomSession.userDataManager.getUserDataByIndex(roomObject.id);
if(!userData) return null;
if(userData.type !== 1) return null;
const type = resolveUserType(userData.type);
return new RoomObjectItem(userData.roomIndex, RoomObjectCategory.UNIT, userData.name, 0, '-', type);
})
.filter(Boolean)
.sort((a, b) => ((a.name < b.name) ? -1 : 1)));
};
useUserAddedEvent(!!items, event =>
{
if(event.id < 0) return;
const userData = GetRoomSession().userDataManager.getUserDataByIndex(event.id);
if(!userData) return;
if(userData.type !== 1) return;
const type = resolveUserType(userData.type);
setItems(prevValue =>
{
const newValue = [ ...prevValue ];
newValue.push(new RoomObjectItem(userData.roomIndex, RoomObjectCategory.UNIT, userData.name, 0, '-', type));
newValue.sort((a, b) => ((a.name < b.name) ? -1 : 1));
return newValue;
});
});
useUserRemovedEvent(!!items, event =>
{
if(event.id < 0) return;
setItems(prevValue =>
{
const newValue = [ ...prevValue ];
for(let i = 0; i < newValue.length; i++)
{
const existingValue = newValue[i];
if((existingValue.id !== event.id) || (existingValue.category !== event.category)) continue;
newValue.splice(i, 1);
break;
}
return newValue;
});
});
const { items, onClose, populateChooser } = useUserChooserState();
const { selectItem } = useUserChooserActions();
return { items, onClose, selectItem, populateChooser };
};
export const useUserChooserWidget = useUserChooserWidgetState;