# Changelog ## React 19 Modernization Phase 2 (2026-05-12) Long-running work on the `feat/react19-modernization` branch — see [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the design rationale. Companion changes shipped on `feat/react19-event-bus` in [`Nitro_Render_V3`](../Nitro_Render_V3) — see that repo's CLAUDE.md for the renderer-side notes. ### Pattern #1: `useNitroEventState` + companions - New `useNitroEventReducer` / `useMessageEventReducer` for the case where multiple event types collapse into one owned state slice. - New `useExternalSnapshot` — typed wrapper of `useSyncExternalStore` pairing the renderer's `EventDispatcher.subscribe()` with `getXxxSnapshot()` getters. - Pilot adoption: `useAvatarInfoWidget` now owns the figure / badges / group merge (three event listeners moved out of `InfoStandWidgetUserView`, three `CloneObject` calls dropped). Reducers extracted to `src/hooks/rooms/widgets/avatarInfo.reducers.ts` with 14 Vitest cases. - `useInventoryFurni` refactored to call three pure reducers (`useInventoryFurni.reducers.ts`) instead of inlining ~250 LOC of merge logic in the event handlers. Module-level `furniMsgFragments` becomes a `useRef` — eliminates a latent bug where two simultaneous client instances would have trampled each other's fragment buffers. Empty `FurniturePostItPlacedEvent` listener dropped. ### Pattern #2: `useNitroQuery` adoption - New `useNitroEventInvalidator(eventType, queryKey, accept?)` companion in `src/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 `useCatalog` into dedicated TanStack queries: - `useGiftConfiguration` (GiftWrappingConfigurationEvent) - `useUserGroups` — consolidates 5 sites that each dispatched `CatalogGroupsComposer` independently - `useClubOffers(windowId)` — per-windowId, with `accept` filter - `useSellablePetPalette(breed)` — per-breed, with `accept` filter - `useMarketplaceConfiguration` — lifted out of a self-fetch in `MarketplacePostOfferView` - `useClubGifts` — paired with `useNitroEventInvalidator` for 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) — `useChatInputState` owns the 5 state slices + 3 event listeners + 3 lifecycle effects; `useChatInputActions` owns `sendChat` with 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; `useWiredToolsStore` internal singleton, public `useWiredToolsState` / `useWiredToolsActions` filter views, `useWiredTools` shim. - **translation** (600 LOC) — 6 consumers; `useTranslationStore` inline + filter views. - **notification** (493 LOC) — ~44 consumers, most of which use a single action (`simpleAlert` or `showConfirm`); 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-motion `Variants` typing on Toolbar + FriendsBar (-33), `useFurniChooserState` retyped as `IRoomObject` + dead `getUserData` guard dropped (-10), React 19 `useRef()` → `useRef(null)` sweep on 15 sites (-15), `IGetImageListener` single-arg signature migration on 3 sites, `ColorVariantType` extended with the 5 `outline-*` 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`: `RoomEnterComposer` now accepts optional `spawnX`/`spawnY` matching Arcturus' `RequestRoomLoadEvent` optional tail; `RoomSettingsData` surfaces the `allowUnderpass` field that Arcturus already emits. Dead `sendWhisperGroupMessage` / `ChatWhisperGroupComposer` reference 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) — `FriendlyTime` with a deterministic `LocalizeText` mock. ### Logic bug fixes (in scope) - `useInventoryFurni`'s module-level `furniMsgFragments` buffer scoped to `useRef`. - `RoomChatHandler.dispatchEvent(RoomSessionChatEvent)` arg order fix in renderer — `chatColours` and `links` slots were swapped. - `PetBreedingMessageParser.bytesAvailable < 12` was a boolean-vs-number bug; replaced with the standard guard pattern. - `useOnClickChat` was passing an extra 8th arg to `showConfirm` (signature only takes 7). - `UserContainerView` was passing `userProfile.friendsCount` (number) to a `LocalizeText` placeholder 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 `localChangeRef` with 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**: `RoomSessionUserBadgesEvent` handler 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 - `activeBadgeCodes` changed from compact `string[]` to fixed-size `(string | null)[]` array. Empty slots are `null` instead 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 `toggleBadge` and 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.slots` config (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 = 5` references. ### InfoStand Double-Click to Remove - Double-clicking a badge in the InfoStand removes it from active badges (own user only). ### Localization - Added `notification.badge.received` key: - IT: "Nuovo Distintivo!" - EN: "New Badge!" - Located in `public/nitro-assets/config/UITexts.json` and `UITexts_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 ```json { "user.badges.max.slots": 5 } ``` Set to `6` to replace the group badge slot with a 6th badge slot.