Commit Graph

356 Commits

Author SHA1 Message Date
simoleo89 8b4308af16 tests: co-locate every Vitest suite next to its subject under src/
Eliminate the parallel `tests/` tree. Each `*.test.ts` / `*.test.tsx`
now sits in the same directory as the module it covers, mirroring its
filename (`Foo.ts` ↔ `Foo.test.ts`). The renderer-SDK mock used by
component / hook tests moves to `src/__mocks__/nitro-renderer.ts` and
the Vitest setup file becomes `src/test-setup.ts` — both still wired
through `vitest.config.mts` exactly as before, only the paths changed.

All 13 suites + 178/178 cases still pass. The production build is
unaffected: rollup only follows imports from `src/index.tsx` and never
crosses into `.test.ts` files, so test code is naturally tree-shaken
out of the bundle. `yarn build` output is byte-for-byte the same on
the user-facing chunks.

tsconfig drops the now-redundant `tests` include entry. CLAUDE.md
'Layout convention' replaces the old `tests/` row with three rows
documenting the new co-located convention, the `__mocks__/` directory
and the `test-setup.ts` entry; ARCHITECTURE.md picks up the same
update. The 'DO NOT CHANGE' qualifier on the layout is preserved —
this rewrite IS the change, decided deliberately to make tests a
first-class part of the source tree rather than a sibling project.
2026-05-16 11:35:03 +02:00
simoleo89 c16ac1d276 wired-tools: hoist UI-only state flags to Zustand store
Move 14 pure UI flags off useState in WiredCreatorToolsView and into a
new feature-local Zustand store (useWiredCreatorToolsUiStore): tab
navigation (isVisible, activeTab, inspectionType, variablesType), modal
open flags (monitor history/info, inspection give, variable manage,
managed give), and the variable-manage / monitor-history filter +
sort + page selectors. The setters accept either a value or a (prev =>
next) updater to preserve the toggle/pagination call sites.

WiredInspectionTabView and WiredVariablesTabView now consume the store
directly for inspectionType / variablesType / isInspectionGiveOpen,
dropping six props from their interfaces. Behaviour is unchanged: every
listener and memo in the parent still reads the same values through
selectors, and the new tests pin the defaults and setter semantics
across the 14 flags.

Derived selection state (selectedFurni, monitorSnapshot, variable
highlight overlays, etc.) intentionally stays in the parent for this
pass — moving those requires moving their listener effects too.
2026-05-16 11:21:10 +02:00
simoleo89 ab93113ce7 widgets: wrap each room + furniture widget in its own WidgetErrorBoundary
The umbrella boundary on RoomWidgetsView caught any widget crash but
unmounted every sibling along with the failing widget — a single bad
parser in ChatWidget would dark out the avatar info, chat input,
doorbell and all furniture overlays until the next remount.

Wraps each of the 13 direct children of RoomWidgetsView (AvatarInfo,
Chat, ChatInput, Doorbell, RoomTools, RoomFilterWords, RoomThumbnail,
FurniChooser, PetPackage, UserChooser, WordQuiz, FriendRequest, plus
the FurnitureWidgets umbrella) and each of the 20 sub-widgets inside
FurnitureWidgetsView in its own named WidgetErrorBoundary. A crash
now silently logs through NitroLogger with the widget name and
renders null for that one widget; every sibling keeps rendering.

The outer umbrella stays as defense-in-depth for the wrapper div and
the listener setup in RoomWidgetsView itself.

Closes the "Per-widget WidgetErrorBoundary wrapping" roadmap item;
updates CLAUDE.md and docs/ARCHITECTURE.md accordingly.
2026-05-14 20:18:38 +02:00
simoleo89 97c9717253 fix(layout-image): guard async image fetch with a request-id ref
LayoutFurniImageView and LayoutAvatarImageView both fired async image
generation (TextureUtils.generateImage / SDK resetFigure callback) and
wrote the result back through setImageElement / setAvatarUrl with only
an isMounted / isDisposed component-level guard. If props changed
twice in rapid succession the older request could resolve last and
overwrite the newer image with a stale one, visible on slow
connections or fast scroll over grids of unique items.

Each effect now captures `const requestId = ++requestIdRef.current`
and threads it into every async callback (TextureUtils.generateImage,
the SDK's resetFigure listener, the cache write). When a callback
fires it bails if `requestIdRef.current !== requestId` — only the
latest effect's callbacks make it past the gate. A stale ENDED for
the previous figure now leaves the cache and the rendered url
unchanged.

Moves both bugs from "Open" to "Recently fixed" in
docs/ARCHITECTURE.md.
2026-05-14 20:10:56 +02:00
simoleo89 9d10e52a55 fix(MainView): collapse CREATED/ENDED listeners into a session-aware reducer
Two independent useNitroEvent listeners updated landingViewVisible from
RoomSessionEvent.CREATED and ENDED with no notion of which session was
active. Under flaky websocket reconnects the events can land out of
order: a stale ENDED for the previous room arrives after CREATED for
the new one, flips landingViewVisible back to true, and the user is
left at the hotel view inside a room (or vice versa) until the next
room change.

Folds both events into one useNitroEventReducer that carries the
tracked sessionId. CREATED sets the id and closes the landing view;
ENDED is applied only when its event.session.roomId matches the
tracked id (or no session is active) — otherwise it's a stale ENDED
for a previous session and is ignored. The reducer companion is the
existing useNitroEventReducer from src/hooks/events, so no new
infrastructure.

Moves the entry in docs/ARCHITECTURE.md from "Open" to "Recently
fixed".
2026-05-14 20:09:23 +02:00
simoleo89 0f9fa1203b catalog: migrate remaining 36 useCatalog() consumers to the three filters
Replaces every direct call to the deprecated useCatalog() shim with the
targeted filter(s) (useCatalogData / useCatalogUiState / useCatalogActions).
Each consumer now subscribes only to the slice it actually reads, which
restores React Compiler memoization and stops catalog-wide re-renders
whenever an unrelated key changes.

Removes the now-unused useCatalog shim from useCatalog.ts and the
shim-specific case in tests/useCatalog.filters.test.tsx. The "all four
hooks observe the same singleton" test becomes "all three filters", since
there is no shim left to compare against. useCatalogFavorites swaps its
internal useCatalog() call for useCatalogUiState() (currentType lives in
the UI slice).

Updates CLAUDE.md and docs/ARCHITECTURE.md to reflect that all 48
historical consumers are migrated and the shim is gone.

Vitest: 162/162 (was 163 — minus the deprecated-shim contract case).
2026-05-14 20:05:44 +02:00
simoleo89 59d6c4cab3 catalog: three-way singleton-filter split + first 3 consumer migrations
Completes the useCatalog decomposition. After the previous commit
extracted the pure helpers, this one splits the singleton-via-useBetween
store into three slice-specific entry points and migrates a handful of
consumers as proof.

`src/hooks/catalog/useCatalog.ts`

- Internal `useCatalogState` → renamed to `useCatalogStore` and is no
  longer exported. The full return shape is unchanged so callers that
  still go through the shim see the exact same object.
- Three new exports built on top of the same `useBetween` instance:
    - `useCatalogData()` — server-driven read-only slice (rootNode,
      offersToNodes, currentPage, currentOffer, frontPageItems,
      searchResult, roomPreviewer, isBusy, catalog localization
      version, Builders Club counters + timers).
    - `useCatalogUiState()` — UI ephemeral state + writers
      (isVisible, pageId, previousPageId, currentType, activeNodes,
      navigationHidden, purchaseOptions, catalogPlaceMultipleObjects,
      plus every `set*` writer including the ones that mutate the
      data slice on user-driven selection).
    - `useCatalogActions()` — imperative operations only
      (openCatalogByType, toggleCatalogByType, activateNode,
      openPageBy{Id,Name,OfferId}, requestOfferToMover,
      selectCatalogOffer, getNodeBy{Id,Name},
      getBuilderFurniPlaceableStatus).
- `useCatalog` is kept as a deprecated shim that returns the full
  historical surface, so the 48 existing consumers compile and run
  unchanged.

Pilot consumer migrations (3 of 48):

- `CatalogBuildersClubStatusView` — Data (furni counters, seconds
  timers) + UiState (currentType).
- `CatalogBreadcrumbView` — UiState (activeNodes) + Actions
  (activateNode).
- `CatalogNavigationItemView` — UiState (currentType) + Actions
  (activateNode).

Tests: `tests/useCatalog.filters.test.tsx` (5 cases).

`useBetween` is mocked via `vi.hoisted` so the four hooks share one
deterministic fake store — rendering the real `useCatalogStore`
would mount ~30 useState calls + open a fresh RoomPreviewer +
subscribe to a dozen renderer events, which is more than these
contract tests need.

- `useCatalogData` exposes exactly its read-only keys.
- `useCatalogUiState` exposes exactly its UI keys + setters.
- `useCatalogActions` exposes exactly its imperative ops (and
  explicitly NOT data fields — proves no leak across slices).
- Singleton identity: callbacks read through the shim are `===` to
  the ones read through the slices.
- Shim surface: the historical key set is still present so
  un-migrated consumers don't silently break.

Suite: 163/163 (was 158/158). `yarn typecheck` green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:50:56 +02:00
simoleo89 fd3ef7875d catalog: extract pure helpers + 34 cases, consume them from useCatalog
First half of the proposed `useCatalog` decomposition. The 1036-line
god-hook still owns the singleton-via-useBetween, but the pure logic
it used to define inline now lives in a dependency-free module so it
can be tested in isolation and reused by future split-out hooks
(`useCatalogData` / `useCatalogUiState` / `useCatalogActions` when
those land).

New module: `src/hooks/catalog/useCatalog.helpers.ts` (222 LOC).

- `normalizeCatalogType(type?)` — coerce the optional catalog type to
  `NORMAL` / `BUILDER`. Was a 5-line `useCallback` with an empty
  dependency array.
- `getOfferProductKeys(offer)` — produces the canonical
  `productType:id:classId` and `productType:class:className` keys
  for the resolved-offer cache.
- `findNodeById` / `findNodeByName` — DFS over the catalog tree,
  root explicitly excluded so callers can't select the synthetic
  root by mistake.
- `getNodesByOfferIdFromMap(offerId, map, onlyVisible)` — extracted
  from the closed-over `getNodesByOfferId`. The `onlyVisible`
  fallback to the full bucket when nothing visible remains is
  preserved.
- `buildCatalogNodeTree(NodeData)` — pulled out of the
  `CatalogPagesListEvent` reducer. Builds the tree and the offerId
  index in one pass; the caller now does `const { rootNode,
  offersToNodes } = buildCatalogNodeTree(parser.root)` instead of
  carrying an inline recursive walker + a local map.
- `resolveBuilderFurniPlaceableStatus(input)` — the placement
  decision tree as a pure function. The hook keeps the
  `GetRoomEngine` / `GetSessionDataManager` reads that count
  non-self, non-moderator visitors (only when the subscription has
  expired) and forwards the resulting `visitorCount` into the
  helper, so the previous early-exit semantics are preserved.

`useCatalog.ts` now imports these and removes ~140 lines of inline
copies. Net hook size: 1036 → 961 LOC. Behavior unchanged.

Tests: `tests/useCatalog.helpers.test.ts` (34 cases).

- `normalizeCatalogType` (4) — BUILDER pass-through, NORMAL
  pass-through, undefined/empty fallback, unknown string fallback.
- `getOfferProductKeys` (5) — both keys, id-only when classId<0,
  class-only when className empty, no-product short-circuit,
  empty productType short-circuit.
- `findNodeById` (5) — null input, root exclusion, immediate child,
  grandchild, miss returns null.
- `findNodeByName` (2) — match by name + root exclusion, miss.
- `getNodesByOfferIdFromMap` (5) — empty map, raw bucket pass-through,
  visible-only filter, fallback when no visible remain, miss.
- `buildCatalogNodeTree` (3) — root depth=0 + empty offer map for a
  leaf-only root, DFS traversal tracks offer→nodes across branch
  and leaf, child.parent === root.
- `resolveBuilderFurniPlaceableStatus` (10) — missing offer,
  not-in-room, owner happy path, non-owner without fallback,
  guild admin with time, furni limit reached, shared-pool override
  ignoring the limit, expired+blocked-by-visitors flag,
  expired+visitor count > 0, expired+empty room is okay.

To support the placement-status test the renderer mock gains real
numeric values for `RoomControllerLevel` (NONE..MODERATOR) and
`RoomObjectCategory` (MINIMUM..MAXIMUM); the previous string-keyed
Proxy stubs made `controllerLevel >= GUILD_ADMIN` evaluate to NaN.

Suite: 158/158 (was 124/124). `yarn typecheck` green.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:42:04 +02:00
simoleo89 b01f09c8ea fix: null-check the set type before reading .paletteID in avatar editor
`buildCategory` was reading `set.paletteID` on the line directly above
the `if(!set || !palette) return null` guard. For categories where
`getSetType()` legitimately returns null (PETS, MISC with no figure
data on the server), this threw "can't access property paletteID, set
is null" and triggered the WidgetErrorBoundary when the user opened
the avatar editor.

Split the guard: bail out as soon as `set` is null, then resolve the
palette, then bail again if that's null too.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:17:06 +02:00
simoleo89 9ef6983632 post cherry-pick: restore useEffectEvent wrapper + fix configuration import
Two typecheck regressions surfaced after pulling duckietm PR #126 onto
this branch:

- NotificationBadgeReceivedBubbleView lost its `useEffectEvent` wrapper
  during conflict resolution. The PR rewrote the effect to use a plain
  `useEffect(..., [activeBadgeCodes.length])`; reintroduce the
  `requestBadgesIfEmpty = useEffectEvent(...)` shape that the rest of
  this branch uses, applied to the renamed activeBadgeCodes selector.
- `src/bootstrap.ts` was importing `GetConfiguration` from the package
  alias `@nitrots/configuration`, which Vite resolves via filesystem
  alias but tsgo does not. Swap to the monolithic
  `@nitrots/nitro-renderer` re-export, matching how App.tsx already
  imports the same symbol.

Both fixes get `yarn typecheck` green again and all 113 Vitest cases
still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 21:16:52 +02:00
duckietm 3a7c9ba940 🆙 Fix wear badge in popup 2026-05-13 21:14:19 +02:00
duckietm 2053c8e015 🆕 Added Reset password / Email and chenge username in user settings 2026-05-13 21:13:31 +02:00
simoleo89 cd8951e536 dev: serve game assets via sirv plugin and pre-init configuration
Restoring `yarn start` from "takes forever" back to seconds.

A previous session had symlinked `public/nitro-assets` and `public/swf`
to a sibling `Nitro-Files/` tree (~177k files) so Vite could serve them
through `publicDir`. The cost was massive: chokidar tried to install a
watcher on every file at startup and the dev server hung for minutes
on Windows. Upstream `duckietm/Nitro-V3` never does this — assets live
on a separate HTTP server referenced by URL in the JSON configs.

Changes:

- Remove the two symlinks under `public/` and add a .gitignore entry
  with a note explaining why they must not come back.
- Add a small Vite plugin (`nitroAssetsServer`) that mounts `sirv` on
  `/nitro-assets/*` and `/swf/*`, reading from
  `../Nitro-Files/{nitro-assets,swf}`. sirv is a connect-style
  middleware that bypasses chokidar entirely, so 177k files no longer
  cost anything at startup. The plugin also wires the same handler
  into `configurePreviewServer` so `yarn preview` keeps working.
- Drop the matching `/nitro-assets` and `/swf` entries from
  `server.proxy` — they had been pointed at the auth proxy on :2096
  which does not expose those paths.
- Disable `login.turnstile.enabled` in `renderer-config.json`. The
  configured sitekey is Cloudflare's "always-passes" test key but the
  widget still requires user interaction and blocks the login flow
  in local dev.

Login flow fixes that fell out of debugging:

- `prepare()` in App.tsx ran twice under React Strict Mode (mount →
  cleanup → mount). The first pass set `setShowLogin(true)`, the
  second raced ahead and fell through to `onSessionExpired()`,
  clobbering the login UI. Guard the effect with
  `lastPrepareTriggerRef` so duplicate runs at the same trigger value
  are skipped while intentional re-runs (after a successful login,
  which bumps `prepareTrigger`) still go through.
- Call `GetConfiguration().init()` from `bootstrap.ts` before
  importing `./index`. The renderer's ConfigurationManager logs
  "Missing configuration key" the first time any key is read against
  an uninitialised store, and components mounted in the first paint
  (login screen, hooks, the renderer warmup) were all hitting that
  path before prepare()'s deferred init landed. Pre-loading the
  config means the store is already populated when React mounts.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-13 20:57:01 +02:00
simoleo89 3c732f1c1a Vitest +14 cases on avatarInfo reducers
The three reducers that drive the InfoStand pilot
(applyUserBadgesUpdate / applyUserFigureUpdate /
applyFavouriteGroupUpdate, in src/hooks/rooms/widgets/avatarInfo.reducers.ts)
have been live for ~10 commits without coverage. They encode
non-trivial branches: 'state not AvatarInfoUser' bail-out,
'event for different user / roomIndex' bail-out, dedup-equality
bail-out, and the clearGroup logic (status === -1 || habboGroupId <= 0).
Add tests pinning every branch.

Two import-tightening tweaks made the reducer module itself
testable in jsdom without dragging the renderer SDK in:

- Renderer event types are now type-only imports — they're erased
  at compile time, so the runtime module load of @nitrots/nitro-renderer
  is skipped. The reducer body only reads plain event fields (no
  ) so this is safe.
- AvatarInfoUser / dedupeBadges / IAvatarInfo come from concrete file
  paths instead of '../../../api' (the barrel pulls in Pixi-bound
  modules via the renderer side-imports).

Tests cover each branch by constructing AvatarInfoUser via the
actual class (so the instanceof guard hits) and casting plain event
objects through  for the typed parameter.

Net Vitest count: 99 -> 113 (8 test files).
2026-05-11 23:04:52 +02:00
simoleo89 9f3cd9bd46 Split useFriends into state + actions via useBetween singleton
useFriends backs ~16 consumers with a friend-list state (5 useStates +
6 message-event listeners) plus 4 imperative entry points
(requestFriend, requestResponse, followFriend, updateRelationship).
Same singleton-filter pattern as useNotification / useWiredTools /
useTranslation.

- useFriendsStore (internal, was useFriendsState) — the previous body
  untouched.
- useFriendsState (public, read-only) — friends arrays, settings,
  derived onlineFriends / offlineFriends, getFriend lookup,
  canRequestFriend guard, plus setDismissedRequestIds (UI-local 'hide
  banner' state).
- useFriendsActions (public, imperative) — requestFriend,
  requestResponse, followFriend, updateRelationship.
- useFriends (deprecated shim) — composes both, preserving the
  historical full-shape return.
2026-05-11 23:00:39 +02:00
simoleo89 5344eaf5c0 Split useNotification into state + actions via useBetween singleton
useNotification is consumed by ~44 sites in the codebase but most of
them only need a single imperative entry point (typically simpleAlert
or showConfirm). The hook also runs ~24 useMessageEvent listeners
internally to translate server events into queued notifications.

Same singleton-filter pattern as useWiredTools / useTranslation:

- useNotificationStore (internal, was useNotificationState) — the
  previous body unchanged. ~30 listeners + 5 state slices + 8 actions
  in one closure.
- useNotificationState (public, read-only) — useBetween filter
  exposing only the three queue arrays (alerts, bubbleAlerts,
  confirms). Used by the global NotificationView renderer.
- useNotificationActions (public, imperative) — useBetween filter
  exposing the 8 entry points: simpleAlert / showNitroAlert /
  showTradeAlert / showConfirm / showSingleBubble +
  closeAlert / closeBubbleAlert / closeConfirm.
- useNotification (deprecated shim) — composes the singleton via
  useBetween, preserving the historical return shape so the 44
  existing call sites keep working.

Also brings CLAUDE.md's 'What's wired up' table up to date with the
splits done this session (chat-input doorbell-style, wired-tools +
translation singleton-filter, plus this notification one) and the
8 useCatalog fetch migrations to TanStack queries.
2026-05-11 22:56:32 +02:00
simoleo89 8b79233059 Extract useCatalogFavorites pure helpers + 16 Vitest cases
The 5 pure functions inside useCatalogFavorites
(normalizeCatalogType, getOffersStorageKey, getPagesStorageKey,
parseOffers, parsePages) handle the v2 -> v3 storage-key migration
that runs once per user the first time they open the v3 client. The
parseOffers branch in particular silently morphs the legacy number[]
shape into IFavoriteOffer[] — exactly the kind of one-shot migration
code that should have coverage so a refactor doesn't break old saves.

Move them into useCatalogFavorites.helpers.ts (sibling file, matching
the WiredCreatorTools / useInventoryFurni.reducers / avatarInfo.reducers
convention). useCatalogFavorites imports them back, plus re-exports
the IFavoriteOffer type from the helper module for the public API.

Both helpers import CatalogType from the concrete file path
('../../api/catalog/CatalogType') rather than the api barrel, so the
test file doesn't drag in the renderer SDK and run aground in jsdom.

Tests cover:
- normalizeCatalogType fallback to NORMAL on undefined/garbage/explicit
- storage-key routing for NORMAL / BUILDER / missing arg
- parseOffers: invalid JSON, non-array, empty array, v2 number[] migration,
  v3 IFavoriteOffer[] passthrough, mixed-array passthrough
- parsePages: invalid JSON, non-array, normal array

Net Vitest count: 83 -> 99 (7 test files).
2026-05-11 22:45:57 +02:00
simoleo89 7b062299de useClubGifts + useNitroEventInvalidator: close the catalogOptions bag
This commit drains the last field out of ICatalogOptions (clubGifts)
and deletes the interface — useCatalog no longer owns a catch-all
mutable object that downstream components stuff data into.

Two pieces:

1) New useNitroEventInvalidator(eventType, queryKey, accept?) — a
   small companion to useNitroQuery for the case where the server
   pushes the same event unprompted (e.g. ClubGiftInfoEvent fires
   both as the response to GetClubGiftInfo and again after the user
   claims a gift via SelectClubGiftComposer). It calls
   queryClient.invalidateQueries() on each matching push so the
   next render of any subscriber triggers a fresh queryFn.

2) New useClubGifts() — useNitroQuery on the ClubGiftInfoEvent
   pair, paired with useNitroEventInvalidator so server-driven
   pushes refresh the cache automatically. CatalogLayoutVipGiftsView
   now consumes the query directly. The local optimistic
   'giftsAvailable--' mutation (which side-effected the parser
   object passed back to the catalog state!) is dropped — the
   server's authoritative ClubGiftInfoEvent push is the single
   source of truth via the invalidator.

useCatalog drops the matching listener + the GetClubGiftInfo dispatch
from the catalog-open effect. ICatalogOptions is now empty and
deleted; the catalogOptions / setCatalogOptions state + return-shape
field are removed from useCatalog along with the import.
2026-05-11 22:38:32 +02:00
simoleo89 9a807bf335 useMarketplaceConfiguration: lift the marketplace config self-fetch
MarketplacePostOfferView was both *the* fetcher and the listener for
MarketplaceConfigurationEvent — it dispatched
GetMarketplaceConfigurationMessageComposer from one effect when item
was set, then routed the response through setCatalogOptions.

useCatalog never touched the field; it was passing through catalogOptions
purely as a transport mechanism for this single component to talk to
itself. Replace with useMarketplaceConfiguration() — staleTime Infinity
(server-side constants for a session), enabled on item, single tidy
data path.

Drops marketplaceConfiguration from ICatalogOptions; with petPalettes
out too, ICatalogOptions is now just { clubGifts }. clubGifts is the
last one and needs invalidation (server pushes ClubGiftInfoEvent after
SelectClubGiftComposer) so it stays put until useNitroEventInvalidator
companion lands.
2026-05-11 22:32:35 +02:00
simoleo89 3947781495 useSellablePetPalette(breed): per-breed TanStack query for pet picker
CatalogLayoutPetView previously read 'catalogOptions.petPalettes' (an
accumulating array of CatalogPetPalette objects keyed by breed) and,
on cache miss, dispatched GetSellablePetPalettesComposer(productData.type)
inline. useCatalog kept the matching SellablePetPalettesMessageEvent
listener that appended each new breed to the array (deduping by breed
identity).

Migrate the request/response pair to a TanStack query parameterized on
breed:

  useSellablePetPalette(breed)
    key:     ['nitro', 'catalog', 'petPalette', breed]
    request: () => new GetSellablePetPalettesComposer(breed)
    parser:  SellablePetPalettesMessageEvent
    accept:  event.getParser().productCode === breed
    select:  build a CatalogPetPalette from parser
    enabled: !!breed (avoid spamming composers before currentOffer is set)
    staleTime: Infinity

The view now derives breed from currentOffer.product.productData.type
and reads 'const { data: petPalette }'. The cache-miss-then-fetch
two-pass effect collapses into a single effect that runs once
petPalette resolves (or clears state when offer/petPalette aren't
ready).

Drops the matching listener from useCatalog, drops petPalettes from
ICatalogOptions, and removes the now-unused CatalogPetPalette /
SellablePetPalettesMessageEvent imports from useCatalog.
2026-05-11 22:28:55 +02:00
simoleo89 2a5b9a4a98 useClubOffers: per-windowId TanStack query for HC offer pages
Two catalog layouts each fire 'new GetClubOffersMessageComposer(windowId)'
on mount and read parser.offers via HabboClubOffersMessageEvent:

  - CatalogLayoutVipBuyView (windowId 1)
  - CatalogLayoutBuildersClubBuyView (windowId 2 / 3, depending on
    the addon variant)

Plus useCatalog used to also listen for HabboClubOffersMessageEvent and
stash the offers in 'catalogOptions.clubOffersByWindowId[windowId]' and
'catalogOptions.clubOffers' (the latter being a backward-compat alias
for windowId 1). Three listeners, three independent requests when all
mounted.

New useClubOffers(windowId) wraps the request/response pair as a
TanStack query keyed by '['nitro', 'catalog', 'clubOffers', windowId]'.
accept(): correlation-key filter (parser.windowId === windowId) so
the same multiplexed event doesn't satisfy the wrong query slot.

Both layouts now read 'const { data: offers = null } = useClubOffers(windowId)';
useCatalog drops the listener, ICatalogOptions drops the
clubOffers / clubOffersByWindowId fields and HabboClubOffersMessageEvent
no longer needs to be imported in useCatalog. The localization-refresh
effect that re-cloned both fields is also dropped — React Query owns
the cache now, and ClubOfferData has no localized strings anyway.
2026-05-11 22:23:19 +02:00
simoleo89 2d9785e931 useUserGroups: consolidate 4 dedup'd CatalogGroupsComposer call sites
Four independent components used to send 'new CatalogGroupsComposer()'
on mount and listen for GuildMembershipsMessageEvent:

  - useCatalog (writing into catalogOptions.groups)
  - CatalogLayoutGuildForumView
  - CatalogGuildSelectorWidgetView
  - WiredSelectorUsersGroupView
  - WiredConditionActorIsGroupMemberView

Each fired its own request and re-listened independently. With four
of them mounted in the wired-tools panel during a builder session,
the same packet went out four times.

New useUserGroups() hook wraps the request/response pair with
useNitroQuery (queryKey ['nitro', 'user', 'groups'], staleTime
Infinity — guild membership is session-stable). All four consumers
now read 'const { data: groups = [] } = useUserGroups()' and React
Query dedups: one composer at the first mount, all subsequent mounts
get the cached array.

Drops 'groups' from ICatalogOptions and the corresponding listener +
prev-state-merge from useCatalog — no remaining consumer reads it.
2026-05-11 22:14:39 +02:00
simoleo89 eeb9cc66a5 Split useTranslation into state + actions via useBetween singleton
Same pattern as the wired-tools split: 600-line useTranslation backs
6 consumers with a wide state + action surface. Split along the
read/write seam:

- useTranslationStore (internal, was the inner useTranslationState) —
  the previous singleton body, untouched except for the rename and a
  doc-comment.
- useTranslationState (public, read-only) — useBetween filter exposing
  settings, the supported-languages list, the loading/loaded flags,
  the detected-language tags, lastError, and the pure getLanguageName
  helper.
- useTranslationActions (public, imperative) — same singleton filter
  exposing updateSettings, ensureSupportedLanguagesLoaded, the four
  translate/queue helpers. Also re-exposes 'settings' because most
  call sites need 'if(settings.enabled)' before dispatching.
- useTranslation (deprecated shim) — composes the singleton via
  useBetween, preserving the historical full-shape return.

applyTextTranslationLocale stays exported from the same module path
so LoginView's import keeps working.

Updates docs/ARCHITECTURE.md proposal #4 section to list the three
new splits (chat-input + wired-tools + translation) alongside the
previous five.
2026-05-11 22:05:51 +02:00
simoleo89 e1f5df6b1c Split useWiredTools into state + actions via useBetween singleton
useWiredTools backs 20 consumers with a 618-line wide state + actions
surface; split it along the read/write seam so it's clear at the
import site whether a view is rendering Wired data or mutating it.

Because the actions need access to setters (setUserVariableAssignments,
setFurniVariableAssignments, ...), this isn't the same pure-action
shape as doorbell/friend-request. Used the useBetween singleton
indirection instead:

- useWiredToolsStore (internal) — the entire previous useWiredToolsState
  body, untouched. State + listeners + effects + actions in one
  closure.
- useWiredToolsState (public, read-only) — useBetween(useWiredToolsStore)
  filtered to the 12 state fields (accountPreferences, roomSettings,
  showInspect/Toolbar booleans, variable definitions+assignments,
  areUserVariablesLoaded).
- useWiredToolsActions (public, imperative) — same singleton filtered
  to the 13 actions (updateAccountPreferences, saveRoomSettings,
  requestUserVariables, assignXxx/removeXxx/updateXxx variable
  helpers, openMonitor / openInspectionForFurni / openInspectionForUser).
- useWiredTools (deprecated shim) — composes both, preserves the
  full historical shape so the 20 existing consumers keep working.

useBetween ensures all four entry points hit the same instance, so the
state + dispatch loop stays a single source of truth. This is also the
shape that a future migration to a Zustand slice would inherit
cleanly — each public hook becomes a slice subscription.
2026-05-11 22:00:31 +02:00
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
simoleo89 68de96cac1 Last-mile typecheck sweep: 3 small bugs
- GuideToolOngoingView classNames clause: classNames(..., 'chat.roomId'
  && 'cursor-pointer') — the property name was quoted so the literal
  string 'chat.roomId' was always-truthy. Unquote to read the actual
  chat.roomId field.
- NavigatorRoomSettingsModTabView: UserProfileIconView userName={ user.userId }
  put a number into the string-typed userName prop; the right prop for
  a numeric id is userId.
- WiredExtraVariableEchoView resolvedVariableEntries: the inline
  fallback-entry literal at the bottom of the useMemo got its kind
  field widened to string (instead of the 'custom' literal needed by
  IWiredVariablePickerEntry). Lift it into a typed const + rename to
  namedFallback to avoid the shadowing of the upstream
  createFallbackVariableEntry result.
2026-05-11 21:46:23 +02:00
simoleo89 0c43377f9a Drop dead 'await success' on fire-and-forget catalog-admin actions
CatalogAdminContext exposes savePage / deletePage / saveOffer /
createOffer / deleteOffer as void-returning fire-and-forget composer
dispatches — they just call SendMessageComposer and let the server
push back later. The Offer/Page edit views were 'await action(data);
if(success) closeForm()' as if the actions returned Promise<boolean>,
but they don't return anything. tsgo flagged the truthiness check on
void.

Drop the await + truthiness — call the action, then close the form
unconditionally. This matches the actual behaviour: closeForm() ran
synchronously after the void anyway. A future PR that wants real
'wait for server confirmation' UX should refactor the context to
return Promise<boolean> (correlated to the response packet via the
pendingActionRef machinery already in place).
2026-05-11 21:46:18 +02:00
simoleo89 f09bb7e67c Pixi v8 alignment in 2 room-widget helpers
- ChooserSelectionVisualizer: sprite.blendMode is BLEND_MODES (string
  enum in Pixi v8: 'normal' | 'add' | 'multiply' | ...). The legacy
  Pixi numeric enum compared against '=== 1' (ADD); switch to '=== "add"'.
- MannequinUtilities.MANNEQUIN_FIGURE was inferred as
  (string | number | number[])[]: the 'hd' / 99999 / [99998] triple
  needs to be a typed tuple [string, number, number[]] so the
  figureContainer.updatePart(string, number, number[]) call resolves.
2026-05-11 21:46:13 +02:00
simoleo89 019295226d Sweep targeted typecheck errors: 11 fixes across 9 files
- ProductImageUtility: 'CatalogPageMessageProductData.I' was clearly a
  placeholder/typo in the WALL branch — getProductCategory's first
  param is FurnitureType, so use the enclosing productType.
- YouTubePlayerView: IRoomUserData has webID, not userId. Two
  spectator/watcher-list sites used the wrong field.
- AvatarInfoWidgetView REQUEST_MANIPULATION handler: avatarInfo is
  IAvatarInfo (union); .category / .id only exist on AvatarInfoFurni.
  Type-guard before reading.
- InfoStandWidgetPetView: deleted the duplicate local 'interface
  AvatarInfoPet' — was shadowing the imported one. Drop AvatarInfoPet
  from the import (local interface stands alone).
- FurnitureExternalImageView: missing GetSessionDataManager import (the
  reportedUserId field reads it inline). Added.
- GroupCreatorView setGroupData call: null values for groupName /
  groupDescription / groupColors / groupBadgeParts where IGroupData
  expects string / number[] / GroupBadgePart[]. Empty defaults. Also
  added the previously-omitted groupHasForum field.
- ContextMenuView + WiredCreatorToolsView: 'return () =>
  ticker.remove(updateOverlays)' — Pixi Ticker.remove() returns the
  ticker, leaking the value to React's EffectCallback cleanup which
  expects 'void | (() => void)'. Wrap in block body.
- Deleted src/components/room/widgets/chat/ChatWidgetWindowView_old.tsx
  — dead code (zero references in the codebase), tripping the
  NitroCardHeaderView onCloseClick prop change.

Net tsgo error count: -11.
2026-05-11 21:34:34 +02:00
simoleo89 71a1586866 Strip dead server-sync from UiSettingsContext + re-export ui-settings
UiSettingsContext referenced UiSettingsLoadComposer /
UiSettingsSaveComposer / UiSettingsDataEvent — none of which exist on
the renderer, and the corresponding Arcturus packet handlers don't
exist either (grep across the emulator turns up zero matches for
'UiSettings'). The feature is real (theme color/image stored in
localStorage works) but the cross-device sync was wired against a
non-existent server endpoint.

Strip the server-bound code path: settings keep persisting to
localStorage as before. The full sync becomes a follow-up that will
need both renderer composer classes AND the Arcturus packet handler
landing together.

Also re-export src/api/ui-settings/ from src/api/index so
InterfaceImageTabView / InterfaceColorTabView can import useUiSettings
+ PRESET_COLORS / THEME_PRESETS via the root barrel as the rest of the
codebase does.

Net tsgo error count: -7 (3 from UiSettingsContext imports + 4 from
InterfaceColor/ImageTabView consumers).
2026-05-11 21:34:23 +02:00
simoleo89 a8065f6cf0 Add optional clone() to IPurchasableOffer
useCatalog's localization-refresh effect calls 'offer?.clone ? offer.clone() : offer'
to mint fresh references when locale strings change. Offer.ts implements
clone() but the interface didn't declare it, so the guarded call broke
tsgo. FurnitureOffer (the lazy wrapper) doesn't implement clone — and
the call site is guarded — so 'clone?(): IPurchasableOffer' (optional)
keeps the interface honest without forcing FurnitureOffer to grow a
no-op clone.

Net tsgo error count: -4.
2026-05-11 21:34:17 +02:00
simoleo89 f57266af03 Update 3 IGetImageListener.imageReady call sites to v8 single-arg signature
IGetImageListener.imageReady(result: IImageResult) takes a single
IImageResult object (with .id, .data, .image), but three call sites in
the client still used the old 3-arg destructure '(id, texture, image)
=> ...'. The renderer's RoomEngine.ts already passes
'new ImageResult(...)' to the listener, so the runtime payload matches
the new contract; the old call-site shape just type-errored.

Migrated:
- LayoutPetImageView (pet thumbnail loader)
- LayoutRoomObjectImageView (furniture thumbnail loader)
- useFurniturePresentWidget (gift box image generator)

Also tightened imageFailed handlers from 'imageFailed: null' to a
proper no-op arrow — the interface requires a callback.
2026-05-11 21:34:08 +02:00
simoleo89 a39aa37231 React 19: useRef<T>() -> useRef<T>(null) across 15 sites
React 19 dropped the no-arg useRef overload — the type-only useRef<T>()
form (no initial value) is gone, every call must pass an initial value.
The codebase had 15 occurrences of useRef<HTMLDivElement>() (DOM ref
pattern) all flagged by tsgo as 'Expected 1 arguments, but got 0'.

Mechanical sweep to useRef<HTMLDivElement>(null) — no behavior change,
React still hands out a ref object with .current set to null at mount.

Net tsgo error count: 57 -> 42.
2026-05-11 21:33:58 +02:00
simoleo89 1083b2ea33 Type useFurniChooserState builders + drop dead getUserData guard
The two helper functions buildWallItem and buildFloorItem took
roomObject as 'any', so 'model.getValue<number>(...)' became an
untyped-function-with-type-args error under tsgo (six hits). Typing
the param as IRoomObject (the renderer's public interface — model is
already typed there) fixes them all at once.

The fallback chain for ownerName was guarded by
'sessionDataManager.getUserData ? sessionDataManager.getUserData(ownerId)?.name : null'
— SessionDataManager.getUserData() does NOT exist on the renderer
(documented in Nitro_Render_V3/CLAUDE.md), so that branch was always
dead. Dropping it removes the four tsgo errors and the misleading
condition.

Net tsgo error count: 90 -> 80.
2026-05-11 21:12:34 +02:00
simoleo89 feba672d08 Sweep small typecheck nits: union expansions + React 19 JSX + extra arg
- ColorVariantType missed the 5 outline-* bootstrap variants
  GroupForumThreadView and GroupForumThreadListView already use; adding
  them clears 4 errors.
- React 19 moved the JSX namespace out of the global scope into the
  react module; WiredNeighborhoodSelectorView referenced JSX.Element
  without importing it.
- showConfirm() takes 7 args; the chat link confirm in useOnClickChat
  passed an 8th 'link' icon arg left over from an older signature.
- LocalizeText placeholder array is string[]; UserContainerView passed
  userProfile.friendsCount (number) — call .toString().

Net tsgo error count: 97 -> 90.
2026-05-11 21:12:34 +02:00
simoleo89 96b61ff67b Fix 4 typecheck errors in createNitroQuery
- Import path for SendMessageComposer pointed at ../SendMessageComposer
  (non-existent); the actual module lives at ../nitro/SendMessageComposer.
  Worked at runtime via Vite alias, broke at tsgo.
- request factory was typed as () => unknown so passing the return into
  SendMessageComposer (which expects IMessageComposer<unknown[]>)
  failed the cast.
- The Pick<NitroQueryConfig, ...> bundle handed to awaitNitroResponse
  included 'key', which isn't part of that subset.
- When no select is provided, resolve(event) leaked TParser through the
  TData channel; cast to TData (the default TData=TParser fallback is
  fine for typed callers, but the explicit-generic case needed it).

Net tsgo error count: 100 -> 97.
2026-05-11 21:12:34 +02:00
simoleo89 b5eeb68b9b Type framer-motion variants as Variants — kill 33 tsgo errors
ToolbarView and FriendsBarView declared their motion variant objects
without a type annotation, so tsgo widened transition.type to 'string'
where framer-motion's Variants narrows it to a literal union (spring /
tween / inertia / etc). Every <motion.div variants={...} /> site flagged
the mismatch.

Annotating the constants as Variants makes the literal inference work
('spring' stays 'spring'); also drops the redundant 'as const' on
staggerDirection now that the parent type pins it.

Net tsgo error count: 133 -> 100.
2026-05-11 21:12:34 +02:00
simoleo89 8e4544c5aa Migrate catalog giftConfiguration to useNitroQuery
The catalog's gift wrapping configuration was loaded by an effect in
useCatalog that fired GetGiftWrappingConfigurationComposer every time
the catalog opened, with the response stuffed into a catalogOptions
slice via setState-in-effect. Migrating to a TanStack query gives us
caching/dedup/loading-state for free on this one-shot session-stable
loader.

- New useGiftConfiguration() hook in src/hooks/catalog/ wraps the
  composer/parser pair with useNitroQuery and staleTime: Infinity
  (the wrapping config never changes within a session).
- CatalogGiftView now reads from the query directly instead of via
  catalogOptions; the useCatalog() call in that component is also
  dropped (no other field was used).
- useCatalog drops the GiftWrappingConfigurationEvent listener and the
  unconditional composer dispatch.
- ICatalogOptions loses the giftConfiguration? field — no remaining
  consumer.

First step toward the docs/ARCHITECTURE.md next-PR item 'Migrate
useCatalog read-only fetches to useNitroQuery'. The clubGifts loader
will follow once useNitroEventInvalidator lands (clubGifts can be
push-updated by the server after SelectClubGiftComposer, so it needs
cache invalidation, not just a one-shot fetch).
2026-05-11 21:12:34 +02:00
simoleo89 8b7bedf534 Pilot: extract useInventoryFurni reducers to a pure module
The four useMessageEvent handlers in useInventoryFurniState (furniture
list add/update, list, removed, plus the dead post-it-placed listener)
were inlined as ~250 LOC of merge logic inside setGroupItems callbacks.
Three things change:

- The three meaningful reducers move to useInventoryFurni.reducers.ts
  as applyFurnitureListAddOrUpdate / applyFurnitureList /
  applyFurnitureListRemoved, plus two helpers clearUnseenFlags and
  refreshGroupItemsLocalization for the existing effect-driven mutations.
  Side effects (CreateLinkEvent, attemptItemPlacement, dispatchAdded)
  are passed in via a ctx object so the reducers stay easy to test.
- The module-level furniMsgFragments buffer becomes a useRef, removing
  a latent bug where two simultaneous client instances would have
  trampled each other's fragments.
- The empty FurniturePostItPlacedEvent handler is dropped (dead code).

useInventoryFurni still owns groupItems via useState so the existing
effect-driven setters (unseen flag reset, localization refresh) keep
working; the message handlers now call setGroupItems(prev =>
applyX(prev, event, ctx)) with the extracted reducers.
2026-05-11 21:11:02 +02:00
simoleo89 559d860a7b Pilot: move InfoStand event listeners to useAvatarInfoWidget owner
InfoStandWidgetUserView previously subscribed to three room-session
events (RSUBE_BADGES, USER_FIGURE, FAVOURITE_GROUP_UPDATE) and pushed
the result back to its parent via a setAvatarInfo prop, with each
handler running CloneObject(prev) before patching one field. Three
issues with that shape:

- CloneObject was deep-cloning the whole AvatarInfoUser shape blindly
  with no class-prototype awareness;
- the three listeners raced on shallow merges across the same prev
  reference in StrictMode dev;
- the subscriptions lived outside the state owner, forcing a prop
  callback barrier per event.

The subscriptions are now in useAvatarInfoWidget — the actual owner of
avatarInfo — and call three pure reducers extracted to
src/hooks/rooms/widgets/avatarInfo.reducers.ts (applyUserBadgesUpdate,
applyUserFigureUpdate, applyFavouriteGroupUpdate). Each reducer returns
the same reference when the event doesn't apply so React bail-outs work.
The clone now constructs a fresh AvatarInfoUser preserving prototype.

dedupeBadges is extracted to its own pure module under src/api/avatar/
so Vitest can cover it without pulling in the renderer.

InfoStandWidgetUserView loses the setAvatarInfo prop (parent updated)
and the CloneObject import.
2026-05-11 21:11:02 +02:00
simoleo89 bb1238a5e5 Add useExternalSnapshot + useNitroEventReducer + useMessageEventReducer hooks
The three companions promised in docs/ARCHITECTURE.md proposal #1
('Companion to add later') are now in src/hooks/events/:

- useExternalSnapshot wraps useSyncExternalStore for the renderer's
  EventDispatcher.subscribe() + getXxxSnapshot() pairing introduced in
  Nitro_Render_V3 2.1.0.
- useNitroEventReducer and useMessageEventReducer mirror the existing
  *State hooks but collapse multiple event types into a single owned
  state slice. The message variant accepts either a single event type
  or an array; subscription is wired through a single useEffect to keep
  the rules-of-hooks happy.
2026-05-11 21:11:02 +02:00
simoleo89 dbafc97e89 Drop unused login dialogs (dead code) + Vitest coverage on FriendlyTime
Two unrelated cleanups grouped because they're both small and safe.

Dead code removal
- src/components/login/components/RegisterDialog.tsx
- src/components/login/components/ForgotDialog.tsx
- src/components/login/components/shared.ts (only consumed by the two
  dialogs above)

These were the older non-Form-Actions versions of the register and
forgot-password dialogs. LoginView.tsx defines its own inline versions
that use `useActionState` + `useFormStatus` (Phase 3 of the React 19
modernization), which are the ones actually rendered. The legacy
files were already documented as dead in docs/ARCHITECTURE.md.

NewsWindow.tsx and the `components/` directory itself stay — NewsWindow
is still imported by LoginView at the bottom of the login flow.

Vitest coverage on FriendlyTime (+12 cases)
- 65 -> 77 passing tests, 5 -> 6 test files.
- LocalizeText is mocked with a deterministic stub
  (`${ key }|${ amount }`) so each assertion can verify both the bucket
  chosen and the rounded amount. The mock also short-circuits the
  transitive renderer-SDK import, which keeps the test runner
  decoupled from the renderer install state.
- Buckets covered: seconds / minutes / hours / days / months / years
  for both `format` and `shortFormat`. Plus: threshold override,
  key-suffix concatenation, half-hour rounding, the raw
  `getLocalization` helper.

Verification
- yarn test: 6 files / 77 cases / ~2s.
- yarn eslint on the new test file: 0 errors / 0 warnings.
- yarn tsc: clean on touched files.
2026-05-11 17:59:46 +00:00
simoleo89 f3442f8aa0 Split useFriendRequestWidget into state + actions (flat hooks layout)
Stesso pattern di doorbell / poll / furni-chooser / user-chooser:
flat split sotto src/hooks/rooms/widgets/, no co-location dentro
src/components/.

Split
- src/hooks/rooms/widgets/useFriendRequestState.ts (new):
  activeRequests state + displayedRequests derived (filter su
  dismissedRequestIds) + due bridge events (user added/removed) +
  un useEffect che riallinea activeRequests quando cambia il set
  di requests dal friends-store. Esporta anche il tipo
  ActiveFriendRequest per consumi futuri.
  Plus: ?. su roomSession e userDataManager per evitare il bug
  pattern "session è null in transition" (vedi PetTrainingPanel,
  precedentemente fixato).
- src/hooks/rooms/widgets/useFriendRequestActions.ts (new):
  hideFriendRequest. Thin adapter sul friends-store
  (setDismissedRequestIds), nessuna subscription.
- src/hooks/rooms/widgets/useFriendRequestWidget.ts: deprecated
  shim che compone i due e preserva
  { displayedRequests, hideFriendRequest } per il consumer
  FriendRequestWidgetView.

Verifica
- yarn eslint sui 4 file toccati: 1 errore pre-esistente
  (set-state-in-effect sul useEffect che ri-derive activeRequests
  da requests — già nel file originale, baseline invariata).
- yarn test: 49/49 passing.
- yarn tsc: clean.

Sequence widget split adesso a 5 (doorbell, poll, furni-chooser,
user-chooser, friend-request). Rimangono: usePetPackageWidget,
useWordQuizWidget, useChatInputWidget, useChatWidget,
useAvatarInfoWidget, useFilterWordsWidget.
2026-05-11 17:47:09 +00:00
simoleo89 85fc82794d 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.
2026-05-11 17:45:31 +00:00
simoleo89 0ae371ee09 Split useFurniChooserWidget into state + actions (flat hooks layout)
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 <feature>/ subfolder),
  while the view sits under src/components/room/widgets/<feature>/.
- 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<T>` 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.
2026-05-11 17:09:41 +00:00
simoleo89 bf84a0c2a6 useNitroQuery: add accept() predicate; migrate two mod-tools chatlog views
Many composer/parser pairs on the Nitro wire are correlation-key based:
the request carries a key (roomId, issueId, etc.) and the response shows
up on the globally-shared event bus, where other components may be
listening for the same parser type with a different key. The previous
useNitroQuery resolved on the FIRST matching parser event regardless of
key — useless for that pattern, which is why two obvious migration
targets (ModToolsChatlogView, CfhChatlogView) were skipped earlier.

Adapter change
- New optional `accept?: (event) => boolean` on NitroQueryConfig.
- In awaitNitroResponse, events for which accept returns false are
  IGNORED rather than resolving the promise. The listener stays
  registered, the timeout still applies. This lets callers do:
    accept: e => e.getParser()?.data.roomId === roomId

Migrations
- src/components/mod-tools/views/room/ModToolsChatlogView.tsx
  - Was: useState<ChatRecordData>(null) + useMessageEvent with
    `if (parser.data.roomId !== roomId) return; setRoomChatlog(...)` +
    a mount-only useEffect dispatching the composer.
  - Now: a single useNitroQuery call keyed on roomId; accept filters
    by roomId; the query is enabled only when roomId is set.
    The composer is no longer re-dispatched on remount within
    staleTime; switching to a different room still triggers a fresh
    fetch because the queryKey changes.
- src/components/mod-tools/views/tickets/CfhChatlogView.tsx
  - Same pattern, keyed on issueId.

Both migrations drop ~15 lines per file (no more local state + manual
listener + manual send) while gaining cache/dedup/loading/error
handling from TanStack Query.

Verification
- yarn eslint on the four files: 1 pre-existing error (the
  IMessageEvent "redundant union" false positive in createNitroQuery
  that we already documented — local sandbox doesn't have the
  renderer SDK installed, so its types resolve as `any`).
- yarn test: 49/49 passing.
- yarn tsc on the four files: clean.
2026-05-11 17:00:06 +00:00
simoleo89 bb09a562f6 Extract Monitor tab JSX into WiredMonitorTabView + drop dead overlays
Third (and final, for now) inline-tab extraction in WiredCreatorToolsView.
With this commit Monitor / Inspection / Variables / Settings are all
sibling components; the parent only orchestrates state.

What moved
- ~60 lines of live JSX (Statistics card, Logs table, "Clear all" +
  "View full logs" buttons) → src/components/wired-tools/WiredMonitorTabView.tsx
- The new component takes 7 typed props (3 data + 4 callbacks), no
  state or effects.

Dead code removed
- The Monitor block also contained three modal-style overlays
  (History / Info / Error info) wrapped in `{ false && ... }` — they
  never rendered. The live versions of those modals are mounted by
  the parent outside the NitroCardView (lines ~3327, ~3393, ~3679 in
  the new layout). Dropping the dead duplicates removes ~115 lines
  and ten otherwise-unused symbol references from the parent.

Impact
- WiredCreatorToolsView.tsx: 3710 → 3544 lines (−166 net).
  Combined with the previous two extractions and the
  types/constants/helpers split in 3c68d97, the file is now down
  from 4493 → 3544 lines (−949, −21%).
- The three tab files are each ~150 lines and trivially scannable.

Conscious non-goals
- No state hoisted to a store yet. The shared-state Zustand slice
  is a separate PR. This commit only relocates JSX.
- Behavior unchanged for live code paths. Removing the
  `{ false && ... }` overlays cannot change behavior because they
  were dead branches; the live overlays at the bottom of the parent
  module are the ones the user actually sees.

Verification
- yarn eslint on the two files: 34 problems baseline, 34 after
  (no new issues introduced).
- yarn test: 49/49 passing.
- yarn tsc on the touched files: clean.
2026-05-11 16:56:56 +00:00
simoleo89 d7d9a7e382 Extract Inspection tab JSX into WiredInspectionTabView component
Second of three slices to break up the WiredCreatorToolsView inline
tab bodies (Variables tab was split in the previous commit; Monitor
remains).

What moved
- 139 lines of inline JSX (`{ activeTab === 'inspection' && <div>
  ... </div> }`) → src/components/wired-tools/WiredInspectionTabView.tsx
- The new component declares 28 typed props grouped by area:
  element-type + preview, keep-selected toggle, variables table,
  inline editor, give-variable popover, remove variable. All state
  and actions arrive from the parent — no internal useState/useEffect.
- The "select variable + start editing" double action at the parent
  is wrapped into a single onSelectInspectionVariable callback so
  the sub-component doesn't need to know about the two setters.
- The renderer-SDK type IWired*VariableDefinition is replaced by a
  structural InspectionGiveDefinition declared in the view file:
  { itemId, name, hasValue }. Keeps the sub-component free of
  renderer-SDK imports.

Impact
- WiredCreatorToolsView.tsx: 3809 → 3710 lines (−99 net). Combined
  with the previous commit, the file is now down 191 lines from the
  4493-line single-monolith it was 6 commits ago.
- Inspection panel JSX is now visually scannable as a file. The
  parent only orchestrates state and passes it down.

Conscious non-goals
- No state hoisted. selectedInspectionVariableKeys, editingVariable,
  isInspectionGiveOpen, inspectionGiveValue etc. all still live in
  the parent useState. The Zustand slice for shared wired-tools state
  is a follow-up PR.
- No behavior change. Same renders, same handlers, same DOM.

Verification
- yarn eslint on the two files: 34 problems baseline, 34 after split
  (the same pre-existing FC<{}> + 5 set-state-in-effect on the parent
  module + react-compiler skip warnings).
- yarn test: 49/49 passing.
- yarn tsc on the two files: clean.

Next: extract the Monitor tab (~176 lines), the last inline tab body.
2026-05-11 16:53:52 +00:00
simoleo89 23fc302b24 Extract Variables tab JSX into WiredVariablesTabView component
Proposal #5 from docs/ARCHITECTURE.md, first slice: split one of the
three remaining inline tab bodies of WiredCreatorToolsView out into
its own file. Same approach the Settings tab has had for a while
(see WiredToolsSettingsTabView).

What moved
- 113 lines of inline JSX (the `{ activeTab === 'variables' && <div>
  ... </div> }` block) → src/components/wired-tools/WiredVariablesTabView.tsx
- The new component is a pure presentation function: 12 typed props,
  no useState, no useEffect, no event subscriptions. It receives:
    * state to render: variablesType, variablePickerDefinitions,
      selectedVariableDefinition, canVariableHighlight,
      isVariableHighlightActive, variableManageCanOpen,
      selectedVariableProperties, selectedVariableTextValues
    * actions to call: onVariablesTypeChange, onPickVariable,
      onToggleVariableHighlight, onOpenManagePanel
- The parent supplies all of them inline at the call site. The
  manage-panel open sequence (request fresh user vars + reset page +
  clear selection + show modal) is closed over into a single
  onOpenManagePanel callback, so the sub-component doesn't need to
  know about its three internal setters.

Impact
- WiredCreatorToolsView.tsx: 3901 → 3809 lines (−92 net). The file
  is still large, but one of the three big inline blocks is gone.
  Monitor (~176 lines) and Inspection (~138 lines) remain inline as
  follow-up PRs.
- The React Compiler now has a smaller file boundary for the
  Variables panel; once the other two blocks come out the parent
  module should stop being skipped for memoization.

Conscious non-goals
- No state was moved. The shared state (selectedVariableKeys,
  isVariableHighlightActive, variableManagePage, etc.) still lives
  in the parent's useState. Hoisting them to a Zustand slice would
  be a separate PR — premature here.
- No behavior change. Same renders, same handlers, same DOM.

Verification
- yarn eslint on the two touched files: 34 problems baseline,
  34 problems after the split (identical: same FC<{}>, same
  pre-existing set-state-in-effect, same react-compiler skip
  warnings on the parent module).
- yarn test: 49/49 passing.
- yarn tsc on the two files: clean.
2026-05-11 16:46:48 +00:00
simoleo89 388fb8ed34 Migrate CatalogLayoutRoomAdsView's room-ad fetch to useNitroQuery
Second concrete adoption of proposal #2 (first was OfferView).

Before
- A useState<RoomEntryData[]>([]) for availableRooms.
- A useMessageEvent<RoomAdPurchaseInfoEvent> handler that
  set the state on each parser event.
- A useEffect on mount that dispatched two composers, one of which
  was GetRoomAdPurchaseInfoComposer paired with the parser above.

After
- A single useNitroQuery call wires the request and parser as one
  read-only query. The select extracts parser.rooms with a default
  empty array.
- staleTime is 60s — opening the same panel within a minute reuses
  the cached value; the composer is not re-dispatched. Useful here
  because the user navigates between catalog tabs.
- The mount-only useEffect no longer dispatches the room-ad composer;
  the second composer (GetUserEventCatsMessageComposer) stays where
  it was — that one feeds useNavigator state and isn't a
  request-response pair this component owns.

Why this file
- It was the cleanest pattern in the catalog tree: no correlation
  keys, no conditional filter on the parser, no other writes to
  availableRooms. The pure derive-from-event case useNitroQuery is
  built for.
- The big god-hook useCatalog (1100 LOC) still owns most of the
  catalog data layer; migrating that needs the data/uiState/actions
  split first.

Verification
- yarn test: 49/49 still passing.
- yarn eslint on the touched file: 1 error (the pre-existing
  set-state-in-effect on line 36, unchanged — baseline matches).
- The previous useMessageEvent import was removed cleanly.
2026-05-11 16:40:43 +00:00