mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 15:06:20 +00:00
9f3cd9bd461544c441386cb8a992434cb5fb2ad6
463 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
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. |
||
|
|
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. |
||
|
|
7cf01b0947 |
docs: refresh ARCHITECTURE + CLAUDE with this session's work
- Pattern #2 (useNitroQuery): list the eight catalog-layer queries carved out of useCatalog this session (gift / groups / club offers / pet palette / marketplace / club gifts), plus the new useNitroEventInvalidator companion for server-push refresh. - Note ICatalogOptions deleted — the legacy 'catalogOptions' bag has no remaining fields after the migrations, useCatalog no longer exposes it. - New 'useCatalog decomposition (in progress)' table — what's been lifted to TanStack and what's deferred (page tree + Builders Club status, both core state slices that move with the data/actions split). - Pattern #4 (god-hook split): add the three new splits done this session (chat-input doorbell-style, wired-tools singleton-filter, translation singleton-filter inline). - Bump Vitest count 83 → 99 (added 16 cases on the useCatalogFavorites helpers). - Note the pure-module test convention: import from concrete file paths rather than the api barrel to avoid jsdom pulling in Pixi. - Typecheck baseline: client now reports 0 too (was 57 at last doc-write); the section's enumerated sweeps all landed. - CLAUDE.md: bump '77/77' references to '99/99' (both places). |
||
|
|
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).
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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). |
||
|
|
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. |
||
|
|
2a9a5ddb64 |
Add react-colorful dep for InterfaceColorTabView
InterfaceColorTabView already imports react-colorful's HexColorPicker but the dep was never installed — tsgo flagged TS2307. Adding it via yarn (5.7.0, the current stable line). |
||
|
|
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. |
||
|
|
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). |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
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.
|
||
|
|
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). |
||
|
|
f1af6fb68a |
docs: ARCHITECTURE pattern #1 — companions implemented, pilots adopted
Updates the proposal #1 section to reflect the four companion hooks now in src/hooks/events/ (useNitroEventReducer, useMessageEventReducer, useExternalSnapshot, on top of the existing *State hooks) and marks the InfoStand + Inventory pilots from the original Fase 2 plan as adopted. Adds the convention note for state owned outside the listener: keep useState + useMessageEvent and extract the reducer as a pure function, citing the two new reducer modules as reference. |
||
|
|
b1729d8ddc |
Vitest: cover dedupeBadges with 6 cases
The badge-deduplication helper was extracted from InfoStandWidgetUserView in the prior commit; it's a pure (badges[]) => badges[] function that keeps slot indices stable by replacing duplicate codes with empty strings. Coverage: - empty input - unique-only passthrough - duplicate-replaced-with-empty - falsy entries (null / undefined / '') normalized to '' - first-occurrence-wins semantics - order sensitivity (same multiset, different order -> different output) |
||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
f75762a2db |
Add CLAUDE.md + refresh docs/ARCHITECTURE.md to current state
Two doc changes so a fresh local Claude Code session can pick up the
branch without re-discovering the conventions and the work-in-progress.
CLAUDE.md (new, repo root)
- Onboarding file Claude Code reads automatically at session start.
- TL;DR with branch name + PR number, points at docs/ARCHITECTURE.md.
- Stack snapshot (React 19, TS 7 native, Vite 8 + Compiler, Zustand 5,
TanStack Query 5, Vitest 3).
- Layout convention spelled out — `src/components/<area>/<feature>/`
for views, `src/hooks/<area>/<feature?>/` flat for hooks. The
rejected-feature-folders decision is the most stepped-on rake, so
it lives here at the top.
- The canonical 3-file god-hook split shape with doorbell as the
reference.
- Patterns to use with copy-pasteable signatures: useNitroEventState,
useMessageEventState, useNitroQuery (with the accept() filter),
Zustand stores via createNitroStore, WidgetErrorBoundary.
- "Wired up vs not yet" matrix: what each pattern is adopted on and
what the next reasonable target is.
- Pointer to the two still-open logic bugs (MainView CREATED/ENDED
race; LayoutFurniImageView async fetch race) with fix shapes.
- House rules: commit author override, no claude/... branch names,
never merge a layout-violating branch, skip-motivated splits are
fine if explained in the commit message.
docs/ARCHITECTURE.md (refresh)
- "What's already in place" rewritten to reflect the full state of
the feat/react19-modernization branch:
* stale references to the old claude/update-react-typescript-He2rs
branch removed
* the three additional god-hook splits done since the last edit
(furni-chooser, user-chooser, friend-request) added
* the 4 useNitroQuery migration sites listed (OfferView,
CatalogLayoutRoomAdsView, ModToolsChatlogView, CfhChatlogView)
* the three additional WiredCreatorToolsView tab extractions
(Monitor, Inspection, Variables) with the 4493 -> 3544 line
counter
* dead-code removal of the legacy login dialogs documented
* the Vitest count updated from 22 to 77 across 6 test files
* usePollSubscriptions hoist to RoomWidgetsView noted
- "How to pick the next refactor PR" rewritten:
* completed items removed (the previous list still had
"hoist usePollSubscriptions" as todo even though it's done,
and "per-tab WiredCreatorTools split" same)
* remaining priorities re-ordered: useCatalog migration (1),
useCatalog split (2), per-widget error boundaries (3),
wired-tools shared-state Zustand slice (4), the two open
logic bugs (5), wider Vitest coverage (6).
* "skipped intentionally" subsection added for the god-hook
splits that need design work first (pet-package, word-quiz,
chat-input, chat-widget, avatar-info).
Verification
- yarn test: 77/77 still passing.
- grep claude/update-react-typescript-He2rs docs/ARCHITECTURE.md: 0
(no stale branch refs).
Now a fresh `claude` session in this repo can read CLAUDE.md, follow
the link to ARCHITECTURE.md, and start contributing without re-asking
the conventions.
|
||
|
|
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.
|
||
|
|
bb28d252d8 |
Vitest: +16 cases on ColorUtils, FixedSizeStack, LocalizeFormattedNumber
49 -> 65 passing tests, 4 -> 5 test files. New file: tests/api-utils-extra.test.ts (16 cases) - LocalizeFormattedNumber (3): zero/NaN/null guard, sub-1000 stays, >=1000 inserts thin-space group separators. - ColorUtils (8): makeColorHex, makeColorNumberHex (with zero-pad), convertFromHex (with/without #), int_to_8BitVals/eight_bitVals_to_int roundtrip, int2rgb pure-RGB output, zero-input edge cases. - FixedSizeStack (4): grow then overwrite oldest (ring-buffer semantics), reset clears state, partial-fill behavior of getMax, empty-stack returns Number.MIN_VALUE. The "partial-fill" case documents a subtle quirk: getMax iterates the whole maxSize window including undefined slots, but `undefined > X` is false in JS so the inserted value wins — the test pins that behavior. Note on `usePetPackageWidget` and `useWordQuizWidget` - They were both considered for a state/actions split this turn but their actions mutate internal state (`onClose` resets 5 useState, `vote` reads pollId/question/answerSent). A clean split would require either passing args to the action or hoisting the state to a shared store first. Deferred as follow-up. Verification - yarn test: 5 files / 65 cases / ~1.9s. - yarn eslint on the new test file: 0 errors / 0 warnings. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
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. |
||
|
|
9d2e4a7324 |
Expand Vitest coverage on the pure helpers in src/api/{utils,wired}
22 -> 49 passing tests, 2 -> 3 test files. Targets are functions with zero external dependencies (no renderer SDK, no network, no DOM). They were picked because: - they're easy to break by accident in a refactor (rounding edge cases, zero-padding rules); - their behavior is documented by tests once and for all, including the surprising bit about LocalizeShortNumber rounding 950..999 into the "1K" bucket (kept as an explicit "documented quirk" assertion rather than fixed — the current behavior is what the rest of the app expects). New file: tests/api-utils.test.ts (27 cases) - ConvertSeconds: zero, 1m / 1h / 1d, mixed, single-digit padding (6). - LocalizeShortNumber: zero/NaN/null guard, sub-1000 stays as-is, K/M/B buckets, negative numbers, the 950..999 rounding quirk (7). - CloneObject: primitives, identity preservation, key fidelity (3). - GetWiredTimeLocale: even (whole sec), odd (half sec), zero (3). - WiredDateToString: zero-pad rules, two-digit values (2). - PrefixUtils.parsePrefixColors: empty inputs, mapping, color reuse (3). - PrefixUtils.getPrefixFontStyle: default empty id, known preset, unknown id (3). Verification - yarn test: 3 files / 49 cases / ~1.1s. - yarn eslint on tests/: 0 errors / 0 warnings. - All test targets are stable pure functions; the assertions double as documentation for callers. |
||
|
|
419de09638 |
Hoist usePollSubscriptions to RoomWidgetsView; drop the side effect from usePollWidget
Follow-up to the previous commit's poll split. The compat shim
usePollWidget used to call usePollSubscriptions() inside its body so
the three RoomSessionPollEvent listeners were still registered for
existing consumers — but that meant:
- listeners would be re-registered per consumer (today nobody, since
useWordQuizWidget was already migrated to usePollActions);
- the lifetime of the subscriptions was tied to a leaf widget instead
of the room session;
- a render of a component using the shim had the side effect of
attaching three global event listeners.
Move
- src/components/room/widgets/RoomWidgetsView.tsx now calls
usePollSubscriptions() once at the top of the room-widget tree. The
bridge from RoomSessionPollEvent (OFFER/ERROR/CONTENT) to the UI
event bus is now mounted for exactly the lifetime of an in-room
session, regardless of which leaf widget renders.
- src/hooks/rooms/widgets/usePollWidget.ts (compat shim) is reduced
to a one-liner that just returns usePollActions(). It is still
deprecated; remove once nothing imports it.
Verification
- yarn eslint on the two touched files: 1 pre-existing error
(the same FC<{}> in RoomWidgetsView that was there before — baseline
unchanged; I deliberately did not touch it in this commit to keep
the diff minimal).
- yarn test: 22/22 still passing.
- grep confirms usePollWidget has zero in-tree consumers; the only
importer is the barrel re-export.
|
||
|
|
7218285583 |
Split usePollWidget into subscriptions + actions (proposal #4) + doc update
usePollWidget bundled two unrelated responsibilities:
- three useNitroEvent listeners that bridge RoomSessionPollEvent
(OFFER / ERROR / CONTENT) onto the UI event bus via DispatchUiEvent
— pure side-effects, zero local state, should mount once;
- three imperative actions (startPoll, rejectPoll, answerPoll) that
every consumer wants, but which shouldn't re-register the listeners.
In practice the only consumer of usePollWidget was useWordQuizWidget,
which needed only `answerPoll` — but pulled in the three subscriptions
as a side effect every time the word-quiz widget rendered. That's the
classic god-hook anti-pattern this proposal targets.
Split (mirrors the doorbell pattern already in place):
- src/hooks/rooms/widgets/usePollSubscriptions.ts (new): the three
bridge listeners, returns void. Should be mounted ONCE at the
highest stable level above poll-aware UI (room widgets root). For
now still mounted by the shim — follow-up PR can move it.
- src/hooks/rooms/widgets/usePollActions.ts (new): the three
imperative actions. Defensive `?.` on roomSession so a poll action
during a room transition no longer crashes.
- src/hooks/rooms/widgets/usePollWidget.ts: kept as a deprecated shim
that composes both — preserves the old `{ startPoll, rejectPoll,
answerPoll }` shape so existing consumers don't break.
- src/hooks/rooms/widgets/useWordQuizWidget.ts: migrated to import
usePollActions directly. The word-quiz widget no longer registers
poll subscriptions transitively — its render no longer has the side
effect of subscribing to three renderer events.
Doc
- docs/ARCHITECTURE.md "What's already in place": records both god-hook
splits (doorbell + poll), the now-enabled React Query and Zustand,
and the test infrastructure. Removes the "not yet enabled" markers
for #2 and #5.
- "How to pick the next refactor PR": rewritten to reflect that the
foundations are done. New priority order:
1. migrate useCatalog's read-only fetches to useNitroQuery,
2. hoist usePollSubscriptions to room-session level,
3. split useCatalog along the doorbell/poll lines,
4. broaden Vitest coverage,
5. per-tab WiredCreatorToolsView split.
Verification
- yarn eslint on the touched files: 0 errors / 0 warnings.
- yarn test: 22/22 passing, 2 files, ~1.0s.
- Existing useWordQuizWidget consumers (RoomWidgetsView ->
WordQuizWidgetView) unaffected — they import from the barrel which
still re-exports the same shape.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
|
||
|
|
6793de2106 |
Set up Vitest + 22 smoke tests on pure modules (proposal #6)
Phase 3 of the refactor plan in docs/ARCHITECTURE.md — the foundation
that unblocks every safe refactor below.
Install
- yarn add -D vitest@3 jsdom @testing-library/dom @testing-library/react
@testing-library/jest-dom
Note: pinned to vitest@3 (not the latest 4.x) because yarn 1's peer
resolution breaks on vitest@4's peer link to vite. With vitest@3 the
existing Vite 8 install resolves cleanly.
Configuration
- vitest.config.mts (new): separate from vite.config.mjs because the
dev/build config wires up renderer SDK aliases that point at sibling
working trees (../renderer, ../Nitro_Render_V3). Tests are written
against pure modules that don't pull in the renderer, so the test
runner uses a smaller alias set.
- tests/setup.ts (new): imports @testing-library/jest-dom/vitest so
custom matchers (toBeInTheDocument, etc.) are available without
per-file imports.
- tsconfig.json: include "tests" so eslint stops complaining about
unparseable files; also makes the IDE see the test files.
- package.json scripts: "test" (one-shot) and "test:watch".
Tests
- tests/WiredCreatorTools.helpers.test.ts (18 cases): covers the pure
helpers extracted in 3c68d97 — createEmptyMonitorSnapshot,
formatMonitorLatestOccurrence (5 time-bucket branches),
formatMonitorHistoryOccurrence, formatVariableTimestamp,
formatMonitorSource (4 branches), normalizeMonitorReason. These are
the most boring-but-easy-to-break functions; locking them down first
is high value, near-zero risk.
- tests/navigatorRoomCreatorStore.test.ts (4 cases): exercises the
Zustand store added in the previous commit — initial state, latch
semantics, 5s auto-reset (with fake timers), and the
"second beginCreate restarts the lockout" invariant. Validates that
the store-based replacement of the let-singleton has the same
observable behavior, plus the new invariant that wasn't possible
before (timer composition under StrictMode double-mount).
Side effect: two non-test source files were converted to `import type`
to keep the test bundle from accidentally pulling in the renderer SDK
transitively:
- src/components/wired-tools/WiredCreatorTools.types.ts
(`import type { AvatarInfoFurni }`)
- src/components/wired-tools/WiredCreatorTools.helpers.ts
(`import type { HotelDateTimeParts, MonitorSnapshot }`)
This is harmless — TypeScript already treated them as type-only —
and improves tree-shaking on build as a side benefit.
Verification
- yarn test -> 2 files, 22 tests passing in ~1.0s.
- yarn eslint on tests/ + the two type-only-import files: 0 errors,
0 warnings.
Migration path
- Next adoption targets: cover useDoorbellState reducer (data hook
split), the new useNitroQuery adapter (timeout/cleanup behavior),
and the smaller pure formatters under src/api/.
- React component tests (via @testing-library/react) deferred until
there's a small mock layer for the renderer SDK. The
@testing-library/* deps are already installed so that PR is
unblocked.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
|
||
|
|
fd1835ca5d |
Enable Zustand (proposal #5) + convert isCreatingRoom singleton
Phase 2 of the refactor plan in docs/ARCHITECTURE.md.
Install
- yarn add zustand (^5, matches React 19 peer requirement).
Wiring
- src/state/createNitroStore.ts: replaces the previous prototype
(which threw on call) with a re-export of zustand's `create` under
the project-local name `createNitroStore`. Comments document the
convention (one store per domain, subscribe to slices not the whole
store).
First migration target
- src/components/navigator/views/navigatorRoomCreatorStore.ts (new):
a Zustand store with `isCreating: boolean` and `beginCreate()` —
the latter latches the flag to true, dispatches an internal
setTimeout to auto-reset after 5s, and replaces any in-flight timer
on re-entry. The timer handle lives in the store's closure, so a
remount of the view doesn't reset the lockout and StrictMode's
double-mount no longer schedules two pending timers.
- src/components/navigator/views/NavigatorRoomCreatorView.tsx:
removes the two module-level `let` variables that the React Compiler
was flagging ("Writing to a variable defined outside a component is
not allowed"). The component now reads `isCreating` via a slice
subscription and calls `beginCreate()` from the click handler. The
imperative guard (`if (isCreating) return`) uses
`useRoomCreatorStore.getState()` so it reads the latest value
synchronously without being a stale closure.
- Also cleans up `FC<{}>` -> `FC` while touching the file.
Verification
- yarn eslint on the three touched files: 1 pre-existing error
(the `setCategory(categories[0].id)` set-state-in-effect on the
categories hook, deliberately left as-is in Phase C — it's the
"init from late-arriving async data" pattern; baseline matches).
- yarn tsc: clean.
Migration path (per docs/ARCHITECTURE.md)
- This is the smallest possible Zustand pilot (~30 lines), chosen
because the let-singleton anti-pattern was the most obvious quick
win and the React Compiler was already complaining about it.
- Next adoption targets (cross-feature UI state): the toolbar's
active-window state (currently inside scattered Contexts), the
notification center's open-state, the catalog's currentPage/selection
state (after the god-hook split).
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
|
||
|
|
34b1b56788 |
Enable React Query (proposal #2) + first real-data pilot on OfferView
Phase 1 of the refactor plan in docs/ARCHITECTURE.md.
Install
- yarn add @tanstack/react-query@5 @tanstack/react-query-devtools@5
- Both pinned to ^5 (matches React 19 peer requirement).
Wiring
- src/index.tsx: mounts QueryClientProvider above ErrorBoundary +
Suspense. Default config: staleTime=30s, retry=1,
refetchOnWindowFocus=false (chat client, not a data dashboard).
Adapter
- src/api/nitro-query/createNitroQuery.ts: replaces the previous
prototype that just threw. Exposes:
* useNitroQuery({ key, request, parser, select, timeoutMs })
— wraps TanStack's useQuery; queryFn awaits the parser response.
* awaitNitroResponse(...) — lower-level helper for imperative use
via queryClient.fetchQuery.
The Promise:
1. registers the parser via GetCommunication().registerMessageEvent
2. dispatches the composer via SendMessageComposer
3. resolves with select(event) on the first matching parser
4. rejects after timeoutMs (default 15s)
5. always cleans up the listener + timeout (cancel-safe).
Pilot
- src/components/catalog/views/targeted-offer/OfferView.tsx:
the previous useMessageEventState + manual useEffect-send pattern
becomes a single useNitroQuery call. staleTime:Infinity because the
targeted offer doesn't change during a session. Subsequent OfferView
remounts (e.g. opening/closing the dialog) now reuse the cached
payload — the GetTargetedOfferComposer is no longer re-sent each
time.
Verification
- yarn eslint on the four touched files: 1 pre-existing
no-redundant-type-constituents error (IMessageEvent resolves as `any`
in the local sandbox without the renderer SDK installed; matches the
12 other pre-existing instances of the same false positive).
- yarn tsc on the four touched files: clean (modulo the
project-wide TS2307 about @nitrots/nitro-renderer).
- The original prototype's "throw" guard is gone — useNitroQuery is now
callable.
Migration path (per docs/ARCHITECTURE.md)
- Next adoption targets (read-only fetches first): useCatalog's page
data, useInventoryFurni's bot listing, Navigator search results,
Marketplace listings.
- Push messages (server-pushed events the client doesn't request)
keep using useNitroEventState / useMessageEventState — they're
subscriptions, not requests.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
|
||
|
|
0755285708 |
Revert feature-folder migration; keep classic src/components + src/hooks layout
Decision: the src/features/<feature>/ layout introduced as proposal #3 (pilot on doorbell in 8ec9d27) is not the convention the team wants. The existing src/components/<area>/ + src/hooks/<area>/ split is the one that stays. What's reverted - src/features/doorbell/ is removed entirely. The doorbell view and the two hooks move back under the classic paths: src/features/doorbell/views/DoorbellWidgetView.tsx -> src/components/room/widgets/doorbell/DoorbellWidgetView.tsx src/features/doorbell/hooks/useDoorbellState.ts -> src/hooks/rooms/widgets/useDoorbellState.ts src/features/doorbell/hooks/useDoorbellActions.ts -> src/hooks/rooms/widgets/useDoorbellActions.ts - The compat shims that lived in those classic paths are dropped now that the real files are back. - src/hooks/rooms/widgets/index.ts adds the two new hooks alongside the existing useDoorbellWidget shim (kept as a deprecated wrapper so any external consumer importing the old shape via the barrel keeps working). What's preserved - The split between data and actions (proposal #4) — useDoorbellState and useDoorbellActions remain two separate hooks. This was the actual improvement, and it's independent of where the files sit. - The bug fixes from 8ec9d27 (close button race, optimistic-remove rollback) — both still present, just in the new path. - src/state/createNitroStore.ts and src/api/nitro-query/createNitroQuery.ts are left where they are. They aren't feature folders; they're cross-cutting framework code (Zustand skeleton, React Query adapter prototype) that any feature can consume. Doc - docs/ARCHITECTURE.md section #3 is rewritten to record the decision rather than recommend the layout. It now describes the convention to follow: * views under src/components/<area>/<feature>/ * hooks under src/hooks/<area>/<feature?>/ (siblings, not subfolders per widget) * sibling .types/.constants/.helpers files for view-specific code (e.g. WiredCreatorTools.*.ts) - "What's already in place" and "Recently fixed" sections updated to point at the new paths. - "How to pick the next refactor PR" no longer mentions feature-folder migration as an option. Note: the five extra feature folders started this session (reconnect, nitropedia, ads, hc-center, campaign) were never committed; they only existed in the working tree and have been restored from HEAD. Verification - find src/features -type f -> 0 (directory removed). - npx tsc --noEmit on all touched files: clean (only the project-wide pre-existing TS2307 about @nitrots/nitro-renderer not installed locally remains, same as before). - npx eslint on all touched files: 0 errors, 0 warnings. https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q |
||
|
|
81656e7b19 |
Fix two logic bugs found while refactoring + document the open ones
These are the bugs surfaced during the structural work that are simple
enough to fix in isolation. Larger ones (race conditions that need
session-token tracking, async-fetch ordering) are deferred and documented
in docs/ARCHITECTURE.md "Known logic bugs" — the repo has Issues
disabled, so the doc is the issue board.
== Fix: room history wiped on every tab close
src/components/room/widgets/room-tools/RoomToolsWidgetView.tsx had a
useEffect that registered a `beforeunload` handler calling
`window.localStorage.removeItem('nitro.room.history')`. The whole point
of localStorage is to persist across sessions; wiping it on tab close is
either a leftover debug call or a misunderstanding of the API.
Removed the handler. History now persists across browser sessions, which
matches user expectations. If "session-only" was the intent, the right
primitive is `sessionStorage` (not localStorage + cleanup) — left as a
note in the doc.
== Fix: AvatarInfoPetTrainingPanelView null-pointer on session change
src/components/room/widgets/avatar-info/AvatarInfoPetTrainingPanelView.tsx
read `roomSession.userDataManager.getPetData(parser.petId)` without
guarding for `roomSession` being null. The PetTrainingPanelMessageEvent
can arrive during a room transition when `roomSession` is briefly null,
crashing the widget. Added `?.` chain on both `roomSession` and
`userDataManager`.
== Doc: known logic bugs section
Two open issues documented for follow-up:
- MainView.tsx CREATED/ENDED race — needs session-token tracking, fits
cleanly into the future useNitroEventReducer companion to proposal #1.
- LayoutFurniImageView / LayoutAvatarImageView async fetch ordering —
needs request-id refs, or solves itself once React Query (proposal #2)
is enabled and the image fetch becomes a query keyed on props.
Plus a "recently fixed" subsection that records the four bugs already
addressed in this branch (doorbell close button, doorbell optimistic
remove, room history wipe, pet panel null-pointer) so the next reader
knows what changed and why.
== Verification
- yarn eslint on the two modified files: same error count before and
after (5 pre-existing set-state-in-effect on RoomToolsWidgetView,
none introduced).
- yarn tsc on the two modified files: clean.
https://claude.ai/code/session_01GrR87LAqnAEyKG2ZbmQt5Q
|