From 2a5b9a4a9839d62802749db3c3e37a82865ce38a Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Mon, 11 May 2026 22:23:19 +0200 Subject: [PATCH] useClubOffers: per-windowId TanStack query for HC offer pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- src/api/catalog/ICatalogOptions.ts | 4 +-- .../CatalogLayoutBuildersClubBuyView.tsx | 13 +++---- .../page/layout/CatalogLayoutVipBuyView.tsx | 16 ++++----- src/hooks/catalog/index.ts | 1 + src/hooks/catalog/useCatalog.ts | 35 +------------------ src/hooks/catalog/useClubOffers.ts | 33 +++++++++++++++++ 6 files changed, 46 insertions(+), 56 deletions(-) create mode 100644 src/hooks/catalog/useClubOffers.ts diff --git a/src/api/catalog/ICatalogOptions.ts b/src/api/catalog/ICatalogOptions.ts index a666d44..4842d97 100644 --- a/src/api/catalog/ICatalogOptions.ts +++ b/src/api/catalog/ICatalogOptions.ts @@ -1,11 +1,9 @@ -import { ClubGiftInfoParser, ClubOfferData, MarketplaceConfigurationMessageParser } from '@nitrots/nitro-renderer'; +import { ClubGiftInfoParser, MarketplaceConfigurationMessageParser } from '@nitrots/nitro-renderer'; import { CatalogPetPalette } from './CatalogPetPalette'; export interface ICatalogOptions { petPalettes?: CatalogPetPalette[]; - clubOffers?: ClubOfferData[]; - clubOffersByWindowId?: Record; clubGifts?: ClubGiftInfoParser; marketplaceConfiguration?: MarketplaceConfigurationMessageParser; } diff --git a/src/components/catalog/views/page/layout/CatalogLayoutBuildersClubBuyView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutBuildersClubBuyView.tsx index 0ea8069..19e8c2c 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutBuildersClubBuyView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutBuildersClubBuyView.tsx @@ -1,9 +1,9 @@ -import { ClubOfferData, GetClubOffersMessageComposer, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer'; +import { ClubOfferData, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { CatalogPurchaseState, LocalizeText, SanitizeHtml, SendMessageComposer } from '../../../../../api'; import { Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, LayoutLoadingSpinnerView, Text } from '../../../../../common'; import { CatalogEvent, CatalogPurchaseFailureEvent, CatalogPurchasedEvent } from '../../../../../events'; -import { useCatalog, usePurse, useUiEvent } from '../../../../../hooks'; +import { useCatalog, useClubOffers, usePurse, useUiEvent } from '../../../../../hooks'; import { CatalogHeaderView } from '../../catalog-header/CatalogHeaderView'; import { CatalogLayoutProps } from './CatalogLayout.types'; @@ -14,12 +14,12 @@ export const CatalogLayoutBuildersClubBuyView: FC = () => { const [ pendingOffer, setPendingOffer ] = useState(null); const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE); - const { currentPage = null, catalogOptions = null } = useCatalog(); + const { currentPage = null } = useCatalog(); const { getCurrencyAmount = null } = usePurse(); const isPurchasingRef = useRef(false); const isAddonLayout = (currentPage?.layoutCode === 'builders_club_addons'); const windowId = (isAddonLayout ? BUILDERS_CLUB_ADDONS_WINDOW_ID : BUILDERS_CLUB_WINDOW_ID); - const offers = catalogOptions?.clubOffersByWindowId?.[windowId] || null; + const { data: offers = null } = useClubOffers(windowId); const onCatalogEvent = useCallback((event: CatalogEvent) => { @@ -120,11 +120,6 @@ export const CatalogLayoutBuildersClubBuyView: FC = () => return currentPage.localization.getText(1) || currentPage.localization.getText(2) || currentPage.localization.getText(0) || ''; }, [ currentPage ]); - useEffect(() => - { - if(!offers) SendMessageComposer(new GetClubOffersMessageComposer(windowId)); - }, [ offers, windowId ]); - useEffect(() => { if(!offers || !offers.length) return; diff --git a/src/components/catalog/views/page/layout/CatalogLayoutVipBuyView.tsx b/src/components/catalog/views/page/layout/CatalogLayoutVipBuyView.tsx index c7b5430..b63f4f6 100644 --- a/src/components/catalog/views/page/layout/CatalogLayoutVipBuyView.tsx +++ b/src/components/catalog/views/page/layout/CatalogLayoutVipBuyView.tsx @@ -1,19 +1,20 @@ -import { ClubOfferData, GetClubOffersMessageComposer, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer'; +import { ClubOfferData, PurchaseFromCatalogComposer } from '@nitrots/nitro-renderer'; import { FC, useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { CatalogPurchaseState, LocalizeText, SanitizeHtml, SendMessageComposer } from '../../../../../api'; import { AutoGrid, Button, Column, Flex, Grid, LayoutCurrencyIcon, LayoutGridItem, LayoutLoadingSpinnerView, Text } from '../../../../../common'; import { CatalogEvent, CatalogPurchaseFailureEvent, CatalogPurchasedEvent } from '../../../../../events'; -import { useCatalog, usePurse, useUiEvent } from '../../../../../hooks'; +import { useCatalog, useClubOffers, usePurse, useUiEvent } from '../../../../../hooks'; import { CatalogLayoutProps } from './CatalogLayout.types'; +const VIP_WINDOW_ID = 1; + export const CatalogLayoutVipBuyView: FC = props => { const [ pendingOffer, setPendingOffer ] = useState(null); const [ purchaseState, setPurchaseState ] = useState(CatalogPurchaseState.NONE); - const { currentPage = null, catalogOptions = null } = useCatalog(); + const { currentPage = null } = useCatalog(); const { purse = null, getCurrencyAmount = null } = usePurse(); - const { clubOffers = null, clubOffersByWindowId = null } = (catalogOptions || {}); - const offers = clubOffersByWindowId?.[1] || clubOffers; + const { data: offers = null } = useClubOffers(VIP_WINDOW_ID); const isPurchasingRef = useRef(false); const onCatalogEvent = useCallback((event: CatalogEvent) => @@ -128,11 +129,6 @@ export const CatalogLayoutVipBuyView: FC = props => } }, [ pendingOffer, purchaseState, purchaseSubscription, getCurrencyAmount ]); - useEffect(() => - { - if(!offers) SendMessageComposer(new GetClubOffersMessageComposer(1)); - }, [ offers ]); - return ( diff --git a/src/hooks/catalog/index.ts b/src/hooks/catalog/index.ts index da4144f..288ef99 100644 --- a/src/hooks/catalog/index.ts +++ b/src/hooks/catalog/index.ts @@ -2,4 +2,5 @@ export * from './useCatalog'; export * from './useCatalogFavorites'; export * from './useCatalogPlaceMultipleItems'; export * from './useCatalogSkipPurchaseConfirmation'; +export * from './useClubOffers'; export * from './useGiftConfiguration'; diff --git a/src/hooks/catalog/useCatalog.ts b/src/hooks/catalog/useCatalog.ts index cbd9cff..a68c36e 100644 --- a/src/hooks/catalog/useCatalog.ts +++ b/src/hooks/catalog/useCatalog.ts @@ -1,4 +1,4 @@ -import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, ClubGiftInfoEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetClubGiftInfo, GetRoomEngine, GetSessionDataManager, GetTickerTime, HabboClubOffersMessageEvent, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, NodeData, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomControllerLevel, RoomEngineObjectPlacedEvent, RoomObjectCategory, RoomObjectPlacementSource, RoomObjectType, RoomObjectVariable, RoomPreviewer, SellablePetPalettesMessageEvent, Vector3d } from '@nitrots/nitro-renderer'; +import { BuildersClubFurniCountMessageEvent, BuildersClubPlaceRoomItemMessageComposer, BuildersClubPlaceWallItemMessageComposer, BuildersClubQueryFurniCountMessageComposer, BuildersClubSubscriptionStatusMessageEvent, CatalogPageMessageEvent, CatalogPagesListEvent, CatalogPublishedMessageEvent, ClubGiftInfoEvent, CreateLinkEvent, FrontPageItem, FurniturePlaceComposer, FurniturePlacePaintComposer, GetCatalogIndexComposer, GetCatalogPageComposer, GetClubGiftInfo, GetRoomEngine, GetSessionDataManager, GetTickerTime, LegacyDataType, LimitedEditionSoldOutEvent, MarketplaceMakeOfferResult, NodeData, ProductOfferEvent, PurchaseErrorMessageEvent, PurchaseFromCatalogComposer, PurchaseNotAllowedMessageEvent, PurchaseOKMessageEvent, RoomControllerLevel, RoomEngineObjectPlacedEvent, RoomObjectCategory, RoomObjectPlacementSource, RoomObjectType, RoomObjectVariable, RoomPreviewer, SellablePetPalettesMessageEvent, Vector3d } from '@nitrots/nitro-renderer'; import { useCallback, useEffect, useRef, useState } from 'react'; import { useBetween } from 'use-between'; import { BuilderFurniPlaceableStatus, CatalogNode, CatalogPage, CatalogPetPalette, CatalogType, DispatchUiEvent, FurniCategory, GetFurnitureData, GetProductDataForLocalization, GetRoomSession, ICatalogNode, ICatalogOptions, ICatalogPage, IPageLocalization, IProduct, IPurchasableOffer, IPurchaseOptions, LocalizeText, NotificationAlertType, Offer, PageLocalization, PlacedObjectPurchaseData, PlaySound, Product, ProductTypeEnum, RequestedPage, SearchResult, SendMessageComposer, SoundNames } from '../../api'; @@ -739,22 +739,6 @@ const useCatalogState = () => }); }); - useMessageEvent(HabboClubOffersMessageEvent, event => - { - const parser = event.getParser(); - - setCatalogOptions(prevValue => - { - const windowId = parser.windowId; - const clubOffersByWindowId = { ...(prevValue.clubOffersByWindowId || {}) }; - - clubOffersByWindowId[windowId] = parser.offers; - - const clubOffers = clubOffersByWindowId[1] || prevValue.clubOffers; - - return { ...prevValue, clubOffers, clubOffersByWindowId }; - }); - }); useMessageEvent(MarketplaceMakeOfferResult, event => @@ -1045,23 +1029,6 @@ const useCatalogState = () => return new CatalogPage(prevValue.pageId, prevValue.layoutCode, prevValue.localization, offers, prevValue.acceptSeasonCurrencyAsCredits, prevValue.mode); }); - setCatalogOptions(prevValue => - { - if(!prevValue) return prevValue; - - const clubOffersByWindowId = { ...(prevValue.clubOffersByWindowId || {}) }; - - Object.keys(clubOffersByWindowId).forEach(key => - { - const offers = clubOffersByWindowId[key]; - - if(Array.isArray(offers)) clubOffersByWindowId[key] = [ ...offers ]; - }); - - const clubOffers = Array.isArray(prevValue.clubOffers) ? [ ...prevValue.clubOffers ] : prevValue.clubOffers; - - return { ...prevValue, clubOffers, clubOffersByWindowId }; - }); }; window.addEventListener('nitro-localization-updated', refreshCatalogLocalization); diff --git a/src/hooks/catalog/useClubOffers.ts b/src/hooks/catalog/useClubOffers.ts new file mode 100644 index 0000000..964f6d4 --- /dev/null +++ b/src/hooks/catalog/useClubOffers.ts @@ -0,0 +1,33 @@ +import { ClubOfferData, GetClubOffersMessageComposer, HabboClubOffersMessageEvent } from '@nitrots/nitro-renderer'; +import { UseQueryResult } from '@tanstack/react-query'; +import { useNitroQuery } from '../../api/nitro-query'; + +/** + * Habbo Club offer list keyed by Catalog `windowId`. windowId 1 is the + * VIP buy page; 2 / 3 are the Builders Club / Builders Club Addons + * pages. Each catalog layout asks the server for its own slice via + * GetClubOffersMessageComposer(windowId) — the server replies with a + * HabboClubOffersMessageEvent carrying parser.windowId + parser.offers. + * + * Wrapped as a TanStack query so multiple consumers reading the same + * windowId share one request, and reopening the page within the + * session-stable cache window doesn't re-fetch. + * + * The accept() predicate filters out responses tagged with a different + * windowId — the renderer multiplexes the same event for every page, + * so without the filter a slow VIP response would land in a Builders + * Club query. + */ +export const useClubOffers = ( + windowId: number, + options: { enabled?: boolean } = {} +): UseQueryResult => + useNitroQuery({ + key: [ 'nitro', 'catalog', 'clubOffers', windowId ], + request: () => new GetClubOffersMessageComposer(windowId), + parser: HabboClubOffersMessageEvent, + accept: event => (event.getParser().windowId === windowId), + select: event => (event.getParser().offers || []), + enabled: options.enabled, + staleTime: Infinity + });