From 0ae371ee09e6adf55086998062ffc086d97162ec Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 11 May 2026 17:09:41 +0000 Subject: [PATCH] Split useFurniChooserWidget into state + actions (flat hooks layout) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Apply the same data/actions split pattern (proposal #4) to useFurniChooserWidget, the largest god-hook still on the widgets side (161 LOC). Layout follows the main branch convention: flat files under src/hooks/rooms/widgets/, no per-feature subfolder, no co-location of hooks inside src/components/. Split - src/hooks/rooms/widgets/useFurniChooserState.ts (new): owns the items array, the populateChooser action that scans the current room, the two RoomEngine event bridges (added/removed), and onClose. Helper buildWallItem/buildFloorItem dedupes the two copies of the RoomObjectItem construction that used to live inline in both populateChooser and the added-event handler (~50 lines of duplication removed). - src/hooks/rooms/widgets/useFurniChooserActions.ts (new): the one pure imperative action — selectItem — that doesn't need to subscribe to anything. - src/hooks/rooms/widgets/useFurniChooserWidget.ts: kept as a deprecated shim that composes both and returns the same { items, onClose, selectItem, populateChooser } shape so FurniChooserWidgetView (the only consumer) doesn't change. Layout note - This is consistent with the main branch: each widget hook is a flat file under src/hooks/rooms/widgets/ (no / subfolder), while the view sits under src/components/room/widgets//. - The parallel feat/react19-hooks-adapter branch chose the opposite convention (hooks co-located inside src/components/...). Per the team decision recorded in docs/ARCHITECTURE.md proposal #3, this repo stays on the flat-hooks-folder layout. Verification - yarn tsc on the touched files: 6 TS2347 errors after the split, 12 before — the buildWallItem/buildFloorItem helpers actually *reduce* the local sandbox TS2347 surface (the renderer SDK is not installed locally, so `roomObject.model.getValue` is flagged as "untyped function with type arg"; merging the two callsites into one helper halves the count). - yarn eslint on the touched files: 0 errors, 0 warnings. - yarn test: 49/49 passing. --- src/hooks/rooms/widgets/index.ts | 2 + .../rooms/widgets/useFurniChooserActions.ts | 16 ++ .../rooms/widgets/useFurniChooserState.ts | 121 +++++++++++++ .../rooms/widgets/useFurniChooserWidget.ts | 167 ++---------------- 4 files changed, 150 insertions(+), 156 deletions(-) create mode 100644 src/hooks/rooms/widgets/useFurniChooserActions.ts create mode 100644 src/hooks/rooms/widgets/useFurniChooserState.ts diff --git a/src/hooks/rooms/widgets/index.ts b/src/hooks/rooms/widgets/index.ts index 9d3f9fb..2906c74 100644 --- a/src/hooks/rooms/widgets/index.ts +++ b/src/hooks/rooms/widgets/index.ts @@ -8,6 +8,8 @@ export * from './useDoorbellState'; export * from './useDoorbellWidget'; export * from './useFilterWordsWidget'; export * from './useFriendRequestWidget'; +export * from './useFurniChooserActions'; +export * from './useFurniChooserState'; export * from './useFurniChooserWidget'; export * from './usePetPackageWidget'; export * from './usePollActions'; diff --git a/src/hooks/rooms/widgets/useFurniChooserActions.ts b/src/hooks/rooms/widgets/useFurniChooserActions.ts new file mode 100644 index 0000000..e20182e --- /dev/null +++ b/src/hooks/rooms/widgets/useFurniChooserActions.ts @@ -0,0 +1,16 @@ +import { GetRoomEngine } from '@nitrots/nitro-renderer'; +import { GetRoomSession, RoomObjectItem } from '../../../api'; + +/** + * Imperative actions for the Furni chooser. Stateless — split from + * useFurniChooserState so components that only need to dispatch a + * selection don't subscribe to the room-object lifecycle events. + */ +export const useFurniChooserActions = () => ({ + selectItem: (item: RoomObjectItem): void => + { + if(!item) return; + + GetRoomEngine().selectRoomObject(GetRoomSession().roomId, item.id, item.category); + } +}); diff --git a/src/hooks/rooms/widgets/useFurniChooserState.ts b/src/hooks/rooms/widgets/useFurniChooserState.ts new file mode 100644 index 0000000..0a03677 --- /dev/null +++ b/src/hooks/rooms/widgets/useFurniChooserState.ts @@ -0,0 +1,121 @@ +import { GetRoomEngine, GetSessionDataManager, RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer'; +import { useState } from 'react'; +import { GetRoomSession, LocalizeText, RoomObjectItem } from '../../../api'; +import { useFurniAddedEvent, useFurniRemovedEvent } from '../engine'; +import { useRoom } from '../useRoom'; + +const isPetOrBot = (roomObjectType: string): boolean => + roomObjectType.includes('pet_') || + roomObjectType.includes('bot_') || + roomObjectType === 'pet' || + roomObjectType === 'bot' || + roomObjectType.includes('rentableBot'); + +const buildWallItem = (roomObject: any): RoomObjectItem | null => +{ + if(roomObject.id < 0 || isPetOrBot(roomObject.type)) return null; + + const sessionDataManager = GetSessionDataManager(); + let name = roomObject.type; + + if(name.startsWith('poster')) + { + name = LocalizeText(`poster_${ name.replace('poster', '') }_name`); + } + else + { + const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + const furniData = sessionDataManager.getWallItemData(typeId); + + if(furniData && furniData.name.length) name = furniData.name; + } + + const ownerId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID) || 0; + const ownerName = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_NAME) || + (sessionDataManager.getUserData ? sessionDataManager.getUserData(ownerId)?.name : null) || + `User_${ownerId}`; + + return new RoomObjectItem(roomObject.id, RoomObjectCategory.WALL, name, ownerId, ownerName, 'furniture'); +}; + +const buildFloorItem = (roomObject: any): RoomObjectItem | null => +{ + if(roomObject.id < 0 || isPetOrBot(roomObject.type)) return null; + + const sessionDataManager = GetSessionDataManager(); + let name = roomObject.type; + + const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); + const furniData = sessionDataManager.getFloorItemData(typeId); + + if(furniData && furniData.name.length) name = furniData.name; + + const ownerId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID) || 0; + const ownerName = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_NAME) || + (sessionDataManager.getUserData ? sessionDataManager.getUserData(ownerId)?.name : null) || + `User_${ownerId}`; + + return new RoomObjectItem(roomObject.id, RoomObjectCategory.FLOOR, name, ownerId, ownerName, 'furniture'); +}; + +/** + * State + event subscriptions for the Furni chooser widget. Pure + * imperative actions (selectItem) live in useFurniChooserActions. + */ +export const useFurniChooserState = () => +{ + const [ items, setItems ] = useState(null); + const { roomSession = null } = useRoom(); + + const onClose = () => setItems(null); + + const populateChooser = () => + { + const wallObjects = GetRoomEngine().getRoomObjects(roomSession.roomId, RoomObjectCategory.WALL); + const floorObjects = GetRoomEngine().getRoomObjects(roomSession.roomId, RoomObjectCategory.FLOOR); + + const wallItems = wallObjects.map(buildWallItem).filter((item): item is RoomObjectItem => item !== null); + const floorItems = floorObjects.map(buildFloorItem).filter((item): item is RoomObjectItem => item !== null); + + setItems([ ...wallItems, ...floorItems ].sort((a, b) => ((a.name < b.name) ? -1 : 1))); + }; + + useFurniAddedEvent(!!items, event => + { + if(event.id < 0) return; + + const roomObject = GetRoomEngine().getRoomObject(GetRoomSession().roomId, event.id, event.category); + + if(!roomObject) return; + + const item = (event.category === RoomObjectCategory.WALL) ? buildWallItem(roomObject) : (event.category === RoomObjectCategory.FLOOR) ? buildFloorItem(roomObject) : null; + + if(item) setItems(prevValue => [ ...(prevValue ?? []), item ].sort((a, b) => ((a.name < b.name) ? -1 : 1))); + }); + + useFurniRemovedEvent(!!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 }; +}; diff --git a/src/hooks/rooms/widgets/useFurniChooserWidget.ts b/src/hooks/rooms/widgets/useFurniChooserWidget.ts index 4116428..0aae3b8 100644 --- a/src/hooks/rooms/widgets/useFurniChooserWidget.ts +++ b/src/hooks/rooms/widgets/useFurniChooserWidget.ts @@ -1,161 +1,16 @@ -import { GetRoomEngine, GetSessionDataManager, RoomObjectCategory, RoomObjectVariable } from '@nitrots/nitro-renderer'; -import { useState } from 'react'; -import { GetRoomSession, LocalizeText, RoomObjectItem } from '../../../api'; -import { useFurniAddedEvent, useFurniRemovedEvent } from '../engine'; -import { useRoom } from '../useRoom'; +import { useFurniChooserActions } from './useFurniChooserActions'; +import { useFurniChooserState } from './useFurniChooserState'; -const isPetOrBot = (roomObjectType: string): boolean => - roomObjectType.includes('pet_') || - roomObjectType.includes('bot_') || - roomObjectType === 'pet' || - roomObjectType === 'bot' || - roomObjectType.includes('rentableBot'); - -const useFurniChooserWidgetState = () => +/** + * @deprecated Use `useFurniChooserState` (data + close + populate) + * and `useFurniChooserActions` (imperative selectItem) directly. + * This shim preserves the `{ items, onClose, selectItem, populateChooser }` + * shape for existing consumers. + */ +export const useFurniChooserWidget = () => { - const [ items, setItems ] = useState(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 sessionDataManager = GetSessionDataManager(); - const wallObjects = GetRoomEngine().getRoomObjects(roomSession.roomId, RoomObjectCategory.WALL); - const floorObjects = GetRoomEngine().getRoomObjects(roomSession.roomId, RoomObjectCategory.FLOOR); - - const wallItems = wallObjects.map(roomObject => - { - if(roomObject.id < 0) return null; - if(isPetOrBot(roomObject.type)) return null; - - let name = roomObject.type; - - if(name.startsWith('poster')) - { - name = LocalizeText(`poster_${ name.replace('poster', '') }_name`); - } - else - { - const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); - const furniData = sessionDataManager.getWallItemData(typeId); - - if(furniData && furniData.name.length) name = furniData.name; - } - - const ownerId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID) || 0; - const ownerName = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_NAME) || - (sessionDataManager.getUserData ? sessionDataManager.getUserData(ownerId)?.name : null) || - `User_${ownerId}`; - - return new RoomObjectItem(roomObject.id, RoomObjectCategory.WALL, name, ownerId, ownerName, 'furniture'); - }).filter(item => item !== null); - - const floorItems = floorObjects.map(roomObject => - { - if(roomObject.id < 0) return null; - if(isPetOrBot(roomObject.type)) return null; - - let name = roomObject.type; - - const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); - const furniData = sessionDataManager.getFloorItemData(typeId); - - if(furniData && furniData.name.length) name = furniData.name; - - const ownerId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID) || 0; - const ownerName = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_NAME) || - (sessionDataManager.getUserData ? sessionDataManager.getUserData(ownerId)?.name : null) || - `User_${ownerId}`; - - return new RoomObjectItem(roomObject.id, RoomObjectCategory.FLOOR, name, ownerId, ownerName, 'furniture'); - }).filter(item => item !== null); - - setItems([ ...wallItems, ...floorItems ].sort((a, b) => ((a.name < b.name) ? -1 : 1))); - }; - - useFurniAddedEvent(!!items, event => - { - if(event.id < 0) return; - - const roomObject = GetRoomEngine().getRoomObject(GetRoomSession().roomId, event.id, event.category); - - if(!roomObject) return; - if(isPetOrBot(roomObject.type)) return; - - let item: RoomObjectItem = null; - - switch(event.category) - { - case RoomObjectCategory.WALL: { - let name = roomObject.type; - - if(name.startsWith('poster')) - { - name = LocalizeText(`poster_${ name.replace('poster', '') }_name`); - } - else - { - const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); - const furniData = GetSessionDataManager().getWallItemData(typeId); - - if(furniData && furniData.name.length) name = furniData.name; - } - - const wallOwnerId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID) || 0; - const wallOwnerName = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_NAME) || - (GetSessionDataManager().getUserData ? GetSessionDataManager().getUserData(wallOwnerId)?.name : null) || - `User_${wallOwnerId}`; - - item = new RoomObjectItem(roomObject.id, RoomObjectCategory.WALL, name, wallOwnerId, wallOwnerName, 'furniture'); - break; - } - case RoomObjectCategory.FLOOR: { - let name = roomObject.type; - - const typeId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_TYPE_ID); - const furniData = GetSessionDataManager().getFloorItemData(typeId); - - if(furniData && furniData.name.length) name = furniData.name; - - const floorOwnerId = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_ID) || 0; - const floorOwnerName = roomObject.model.getValue(RoomObjectVariable.FURNITURE_OWNER_NAME) || - (GetSessionDataManager().getUserData ? GetSessionDataManager().getUserData(floorOwnerId)?.name : null) || - `User_${floorOwnerId}`; - - item = new RoomObjectItem(roomObject.id, RoomObjectCategory.FLOOR, name, floorOwnerId, floorOwnerName, 'furniture'); - } - } - - if(item) setItems(prevValue => [ ...prevValue, item ].sort((a, b) => ((a.name < b.name) ? -1 : 1))); - }); - - useFurniRemovedEvent(!!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 } = useFurniChooserState(); + const { selectItem } = useFurniChooserActions(); return { items, onClose, selectItem, populateChooser }; }; - -export const useFurniChooserWidget = useFurniChooserWidgetState;