Three top-level files brought in sync with the work landed on feat/react19-modernization: - CHANGELOG.md gets a 'React 19 Modernization Phase 2 (2026-05-12)' section spanning all four pattern groups (event-state companions, TanStack queries on the catalog layer, god-hook splits in the doorbell + singleton-filter styles, Pixi v8 / TS 5.7+ alignment), the Vitest growth 65 -> 113, and the in-scope logic bug fixes. - ARCHITECTURE.md bumps the test ledger 99 -> 113 (adds the avatar-info reducer suite), documents the new pure-module test convention (concrete file paths + 'import type' for renderer event types), and lists the two new singleton-filter splits (notification, friends). - CLAUDE.md mirrors the same updates plus a 'Singleton-filter split' recipe alongside the doorbell-style one; useNitroEventInvalidator is documented next to useNitroQuery; the 'What's wired up' table enumerates all 10 split hooks. Test count bumped 99 -> 113 in both the 'Vitest' row and the green-bar house rule.
11 KiB
Changelog
React 19 Modernization Phase 2 (2026-05-12)
Long-running work on the feat/react19-modernization branch — see
docs/ARCHITECTURE.md for the design rationale.
Companion changes shipped on feat/react19-event-bus in
Nitro_Render_V3 — see that repo's CLAUDE.md
for the renderer-side notes.
Pattern #1: useNitroEventState + companions
- New
useNitroEventReducer/useMessageEventReducerfor the case where multiple event types collapse into one owned state slice. - New
useExternalSnapshot— typed wrapper ofuseSyncExternalStorepairing the renderer'sEventDispatcher.subscribe()withgetXxxSnapshot()getters. - Pilot adoption:
useAvatarInfoWidgetnow owns the figure / badges / group merge (three event listeners moved out ofInfoStandWidgetUserView, threeCloneObjectcalls dropped). Reducers extracted tosrc/hooks/rooms/widgets/avatarInfo.reducers.tswith 14 Vitest cases. useInventoryFurnirefactored to call three pure reducers (useInventoryFurni.reducers.ts) instead of inlining ~250 LOC of merge logic in the event handlers. Module-levelfurniMsgFragmentsbecomes auseRef— eliminates a latent bug where two simultaneous client instances would have trampled each other's fragment buffers. EmptyFurniturePostItPlacedEventlistener dropped.
Pattern #2: useNitroQuery adoption
- New
useNitroEventInvalidator(eventType, queryKey, accept?)companion insrc/api/nitro-query/— invalidates a query slot every time the renderer pushes the matching parser event. Required when the server refreshes data outside the request cycle (e.g. ClubGiftInfoEvent after a gift claim). - Seven catalog fetches lifted out of
useCataloginto dedicated TanStack queries:useGiftConfiguration(GiftWrappingConfigurationEvent)useUserGroups— consolidates 5 sites that each dispatchedCatalogGroupsComposerindependentlyuseClubOffers(windowId)— per-windowId, withacceptfilteruseSellablePetPalette(breed)— per-breed, withacceptfilteruseMarketplaceConfiguration— lifted out of a self-fetch inMarketplacePostOfferViewuseClubGifts— paired withuseNitroEventInvalidatorfor the server-push-after-SelectClubGift case
ICatalogOptions(the "catalogOptions" bag that the various views stuffed their fetched data into) is now empty and deleted.
Pattern #4: god-hook splits
Five new splits in this round, two patterns. The doorbell-style (state + actions + shim, no shared singleton) for hooks whose actions are pure-dispatch:
- chat-input (334 LOC → 3 files) —
useChatInputStateowns the 5 state slices + 3 event listeners + 3 lifecycle effects;useChatInputActionsownssendChatwith the full slash-command repertoire and the outgoing-translation pipeline. Single consumer (ChatInputView) keeps the original tuple via the shim.
The useBetween singleton-filter style for hooks where actions
mutate shared state:
- wired-tools (618 LOC) — 20 consumers;
useWiredToolsStoreinternal singleton, publicuseWiredToolsState/useWiredToolsActionsfilter views,useWiredToolsshim. - translation (600 LOC) — 6 consumers;
useTranslationStoreinline + filter views. - notification (493 LOC) — ~44 consumers, most of which use a
single action (
simpleAlertorshowConfirm); the read-only state slice exposes the three queue arrays for the renderer view layer. - friends (258 LOC) — 16 consumers; state slice covers the friend
list / settings / derived online-offline split, actions slice covers
requestFriend/requestResponse/followFriend/updateRelationship.
Documented skip-motivated splits: useChatWidget,
useChatCommandSelector, useFurniturePresentWidget,
useAvatarInfoWidget, useNavigator, useMessenger,
usePetPackageWidget, useWordQuizWidget. Reasons logged in commit
messages.
Typecheck / Pixi v8 / Arcturus alignment
- Repository-wide
tsgo(TS 7 preview) error count: 134 → 0 client, 24 → 0 renderer. Notable clusters: framer-motionVariantstyping on Toolbar + FriendsBar (-33),useFurniChooserStateretyped asIRoomObject+ deadgetUserDataguard dropped (-10), React 19useRef<T>()→useRef<T>(null)sweep on 15 sites (-15),IGetImageListenersingle-arg signature migration on 3 sites,ColorVariantTypeextended with the 5outline-*bootstrap variants. - Renderer-side aligned with Pixi v8 (Filter[] narrowing, WebGLRenderer narrowing, ImageLike cast) and TS 5.7+ ArrayBuffer drift (BinaryReader / BinaryWriter / WsSessionCrypto / NitroBundle).
- Cross-repo additions on
Nitro_Render_V3:RoomEnterComposernow accepts optionalspawnX/spawnYmatching Arcturus'RequestRoomLoadEventoptional tail;RoomSettingsDatasurfaces theallowUnderpassfield that Arcturus already emits. DeadsendWhisperGroupMessage/ChatWhisperGroupComposerreference removed.
Vitest coverage
Bumped from 65 → 113 cases across 8 test files. New coverage:
dedupeBadges.test.ts(6) — slot-preserving badge dedup.catalog-favorites.helpers.test.ts(16) — v2→v3 localStorage migration + per-catalog-type storage-key routing.avatar-info-reducers.test.ts(14) — three reducer bail-out branches + apply paths.friendly-time.test.ts(12) —FriendlyTimewith a deterministicLocalizeTextmock.
Logic bug fixes (in scope)
useInventoryFurni's module-levelfurniMsgFragmentsbuffer scoped touseRef.RoomChatHandler.dispatchEvent(RoomSessionChatEvent)arg order fix in renderer —chatColoursandlinksslots were swapped.PetBreedingMessageParser.bytesAvailable < 12was a boolean-vs-number bug; replaced with the standard guard pattern.useOnClickChatwas passing an extra 8th arg toshowConfirm(signature only takes 7).UserContainerViewwas passinguserProfile.friendsCount(number) to aLocalizeTextplaceholder array (expects string).
Badge System Rework (2026-04-04)
Bug Fixes
- Slot 0 drag bug: Dragging from slot 0 no longer causes badges to disappear. The root cause was
'0'being falsy in JavaScript, which made the drop handler take the wrong code path and overwrite the target badge. - Badge duplication: Fixed badges appearing in multiple slots when dragging in the InfoStand. The issue was a stale props fallback — after a drag operation, the hook updated correctly but the component fell back to old server props for empty slots, showing ghost copies.
- Race condition: Replaced single boolean
localChangeRefwith a counter (pendingUpdatesRef) to correctly handle rapid sequential drag operations without the server overwriting local state. - Badge deduplication:
toFixedSlots()now deduplicates badges, preventing the same badge from appearing in multiple slots even if the server returns duplicates. - Server badge dedup in InfoStand:
RoomSessionUserBadgesEventhandler now deduplicates badges from the server before updating the avatar info.
Drag & Drop Visual Feedback
- Custom drag preview: Badge image is used as the drag ghost instead of the browser default (via
setDragImage). - Source opacity: The dragged item becomes semi-transparent (
opacity-40) during drag. - Pulsing glow on drop targets: Valid drop targets pulse with a blue glow animation (
animate-pulse-glow). - Drop settle animation: A brief scale-down animation (
animate-drop-settle, 300ms) plays when a badge lands in a slot. - Remove indicator: Dragging an active badge over the inventory area shows a red pulsing background with a trash icon overlay.
- Grab cursor: All draggable badge elements now show
cursor-grab/cursor-grabbing.
Sparse Slot Support
activeBadgeCodeschanged from compactstring[]to fixed-size(string | null)[]array. Empty slots arenullinstead of being collapsed, allowing gaps between badges.- All operations (
setBadgeAtSlot,removeBadge,reorderBadges,swapBadges,toggleBadge) work on the fixed-size array without compaction.
New Badge Glow (Feature)
- Unseen (newly received) badges in the inventory now pulse with a gold glow (
animate-pulse-glow-gold) instead of the previous flat green background. - The glow disappears when the badge is selected (unseen status cleared).
Badge Received Toast Notification (Feature)
- When a new badge is received, a bubble notification appears with:
- Badge image and localized name
- "Indossa" / "Wear" button that directly equips the badge via
toggleBadgeand closes the notification - "Non ora" / "Later" link to dismiss
- Auto-fades after 8 seconds (standard bubble behavior).
- Uses the existing
NotificationBubbleType.BADGE_RECEIVED(was defined but unused). - New component:
NotificationBadgeReceivedBubbleView.
Dynamic Badge Slot Count
- Badge slot count is now fully driven by
user.badges.max.slotsconfig (default: 5).- 5 slots: 5 badge slots + group badge in InfoStand (6 boxes total)
- 6 slots: 6 badge slots, group badge is replaced by the 6th slot
- Both the inventory grid and InfoStand layout adapt automatically.
- Removed all hardcoded
maxSlots = 5references.
InfoStand Double-Click to Remove
- Double-clicking a badge in the InfoStand removes it from active badges (own user only).
Localization
- Added
notification.badge.receivedkey:- IT: "Nuovo Distintivo!"
- EN: "New Badge!"
- Located in
public/nitro-assets/config/UITexts.jsonandUITexts_en.json.
Files Modified
| File | Changes |
|---|---|
src/hooks/inventory/useInventoryBadges.ts |
Sparse slots, dedup, race condition fix, toFixedSlots |
src/hooks/notification/useNotification.ts |
BadgeReceivedEvent listener |
src/components/inventory/views/badge/InventoryBadgeView.tsx |
Visual feedback, dynamic maxSlots, fix '0' falsy |
src/components/inventory/views/badge/InventoryBadgeItemView.tsx |
Drag preview, opacity, cursor |
src/components/room/widgets/avatar-info/infostand/InfoStandBadgeSlotView.tsx |
Visual feedback, double-click remove, no stale props |
src/components/room/widgets/avatar-info/infostand/InfoStandWidgetUserView.tsx |
Dynamic layout, server badge dedup |
src/components/notification-center/views/bubble-layouts/GetBubbleLayout.tsx |
BADGE_RECEIVED routing |
src/components/notification-center/views/bubble-layouts/NotificationBadgeReceivedBubbleView.tsx |
New component |
src/layout/InfiniteGrid.tsx |
Gold glow for unseen items |
tailwind.config.js |
Custom keyframes and animations |
Configuration
{
"user.badges.max.slots": 5
}
Set to 6 to replace the group badge slot with a 6th badge slot.