diff --git a/CLAUDE.md b/CLAUDE.md index b4300f4..1475060 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -155,19 +155,20 @@ Login / Register / Forgot in `src/components/login/LoginView.tsx` use | Adopted | Pilot sites | |---|---| -| `useNitroEventState` | `OfferView` | -| `useNitroQuery` | `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, `CfhChatlogView` | +| `useNitroEventState` + companions (Reducer, ExternalSnapshot) | `OfferView`, `useAvatarInfoWidget` (figure/badges/group reducer), `useInventoryFurni` (pure reducers + fragments useRef) | +| `useNitroQuery` + `useNitroEventInvalidator` | `OfferView`, `CatalogLayoutRoomAdsView`, `ModToolsChatlogView`, `CfhChatlogView`, `useGiftConfiguration`, `useUserGroups`, `useClubOffers(windowId)`, `useSellablePetPalette(breed)`, `useMarketplaceConfiguration`, `useClubGifts` (with invalidator) | | Zustand | `NavigatorRoomCreatorView` (`useRoomCreatorStore`) | -| God-hook split | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request` | +| God-hook split (state + actions + shim) | `doorbell`, `poll`, `furni-chooser`, `user-chooser`, `friend-request`, `chat-input` | +| God-hook split (`useBetween` singleton + state filter + actions filter + shim) | `wired-tools`, `translation` | | `WidgetErrorBoundary` | `RoomWidgetsView` umbrella | | Vitest | 99/99 cases on pure helpers + the Zustand store | | Not yet | Notes | |---|---| -| Split `useCatalog` (~1100 LOC) | Migrate read-only fetches to `useNitroQuery` first, then split into `useCatalogData` / `useCatalogUiState` / `useCatalogActions`. | -| Split `useChatInputWidget` / `useChatWidget` / `useAvatarInfoWidget` | Large state machines; needs careful per-file design before mechanical split. | -| Split `usePetPackageWidget` / `useWordQuizWidget` | Their "actions" mutate internal state; need to either pass args or hoist state to a store first. Documented in commit messages, skipped intentionally. | -| Hoist Wired Creator Tools shared state to a Zustand slice | Would remove ~25 props passed to the 3 tab sub-components. | +| Core `useCatalog` split | Session-stable secondary fetches all migrated to TanStack queries (see ARCHITECTURE.md). What's left: core `rootNode`/`offersToNodes`/`currentPage` slice + Builders Club status. Needs a dedicated `useCatalogData`/`useCatalogUiState`/`useCatalogActions` split. | +| Split `useChatWidget` / `useAvatarInfoWidget` | Both state-driven via events with no clean imperative actions to extract — skip-motivated. Already touched today for the InfoStand listener move. | +| Split `usePetPackageWidget` / `useWordQuizWidget` / `useChatCommandSelector` | Their "actions" mutate internal state or are tightly interdependent — skip-motivated. | +| Hoist Wired Creator Tools shared state to a Zustand slice | Would remove ~25 props passed to the 3 tab sub-components. (Wired-tools split done as singleton-filter; Zustand slice is the next step.) | | Wider Vitest coverage (React components) | `@testing-library/*` is installed; needs a small renderer-SDK mock layer first. | ## Known open logic bugs diff --git a/src/hooks/notification/useNotification.ts b/src/hooks/notification/useNotification.ts index 66ac864..7458714 100644 --- a/src/hooks/notification/useNotification.ts +++ b/src/hooks/notification/useNotification.ts @@ -16,7 +16,19 @@ const getTimeZeroPadded = (time: number) => let modDisclaimerTimeout: ReturnType = null; const recentBadgeNotifications = new Set(); -const useNotificationState = () => +/** + * Internal singleton state + actions for the notification subsystem. + * Public consumers should reach for useNotificationState (read-only — + * the queue arrays for the renderer) or useNotificationActions (the + * imperative simpleAlert / showConfirm / showSingleBubble / etc.). + * useNotification is the legacy shim that composes both. + * + * Wrapped in useBetween at each public-hook layer so all consumers see + * the same instance, matching the previous useBetween(useNotificationState) + * behavior — required because ~30 useMessageEvent listeners live inside + * this hook and need to register exactly once across the tree. + */ +const useNotificationStore = () => { const [ alerts, setAlerts ] = useState([]); const [ bubbleAlerts, setBubbleAlerts ] = useState([]); @@ -490,4 +502,58 @@ const useNotificationState = () => return { alerts, bubbleAlerts, confirms, simpleAlert, showNitroAlert, showTradeAlert, showConfirm, showSingleBubble, closeAlert, closeBubbleAlert, closeConfirm }; }; -export const useNotification = () => useBetween(useNotificationState); +/** + * Read-only slice of the notification store: the three queue arrays + * (alerts, bubbleAlerts, confirms) that the renderer view layer drains. + * + * Consumers that only need to *show* a notification should use + * useNotificationActions instead — the queues are an implementation + * detail of the global NotificationView component. + */ +export const useNotificationState = () => +{ + const { alerts, bubbleAlerts, confirms } = useBetween(useNotificationStore); + + return { alerts, bubbleAlerts, confirms }; +}; + +/** + * Imperative slice of the notification store: 8 entry points covering + * the alert / bubble / confirm / trade-alert flows plus the matching + * close handlers. ~40 consumers across the codebase only use one or + * two of these — splitting the slice off keeps their dependency + * surface honest and makes it greppable which call sites + * dismiss-vs-show. + */ +export const useNotificationActions = () => +{ + const { + simpleAlert, + showNitroAlert, + showTradeAlert, + showConfirm, + showSingleBubble, + closeAlert, + closeBubbleAlert, + closeConfirm + } = useBetween(useNotificationStore); + + return { + simpleAlert, + showNitroAlert, + showTradeAlert, + showConfirm, + showSingleBubble, + closeAlert, + closeBubbleAlert, + closeConfirm + }; +}; + +/** + * @deprecated Prefer `useNotificationState` (queue arrays) and + * `useNotificationActions` (imperative show/close helpers) directly. + * This shim composes both into the historical `useNotification()` + * shape so the existing 40+ consumers keep working unchanged. + */ +export const useNotification = () => useBetween(useNotificationStore);