🆕 Redesign of HC Club buy, now also give as gift

This commit is contained in:
duckietm
2026-05-21 14:00:03 +02:00
parent 690a196d42
commit 49917ed49b
5 changed files with 234 additions and 97 deletions
+33 -28
View File
@@ -1,33 +1,38 @@
import { ClubOfferData, GetClubOffersMessageComposer, HabboClubOffersMessageEvent } from '@nitrots/nitro-renderer';
import { UseQueryResult } from '@tanstack/react-query';
import { useNitroQuery } from '../../api/nitro-query';
import { useEffect } from 'react';
import { SendMessageComposer } from '../../api';
import { useMessageEventState } from '../events';
const offersCache = new Map<number, ClubOfferData[]>();
/**
* 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<ClubOfferData[]> =>
useNitroQuery<HabboClubOffersMessageEvent, ClubOfferData[]>({
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
});
): { data: ClubOfferData[] | null } =>
{
const enabled = options.enabled !== false;
const data = useMessageEventState<HabboClubOffersMessageEvent, ClubOfferData[] | null>(
HabboClubOffersMessageEvent,
event =>
{
const parser = event.getParser();
if(!parser || parser.windowId !== windowId) return offersCache.get(windowId) ?? null;
const offers = parser.offers || [];
offersCache.set(windowId, offers);
return offers;
},
() => offersCache.get(windowId) ?? null
);
useEffect(() =>
{
if(!enabled) return;
if(offersCache.has(windowId)) return;
SendMessageComposer(new GetClubOffersMessageComposer(windowId));
}, [ enabled, windowId ]);
return { data };
};
+1 -42
View File
@@ -15,19 +15,6 @@ const getTimeZeroPadded = (time: number) =>
let modDisclaimerTimeout: ReturnType<typeof setTimeout> = null;
const recentBadgeNotifications = new Set<string>();
/**
* 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<NotificationAlertItem[]>([]);
@@ -242,7 +229,6 @@ const useNotificationStore = () =>
{
const parser = event.getParser();
// Skip if BadgeReceivedEvent already showed a notification for this badge
if(recentBadgeNotifications.has(parser.data.badgeCode)) return;
recentBadgeNotifications.add(parser.data.badgeCode);
@@ -258,7 +244,6 @@ const useNotificationStore = () =>
{
const parser = event.getParser();
// Skip if AchievementNotificationMessageEvent already showed a notification for this badge
if(recentBadgeNotifications.has(parser.badgeCode)) return;
recentBadgeNotifications.add(parser.badgeCode);
@@ -266,9 +251,6 @@ const useNotificationStore = () =>
const badgeName = LocalizeBadgeName(parser.badgeCode);
const badgeImage = GetSessionDataManager().getBadgeUrl(parser.badgeCode);
// senderName is non-empty only when a staff member awarded the badge
// via the `:badge` command. Empty for achievements, catalog buys,
// wired rewards, poll rewards, etc.
const senderName = parser.senderName || '';
showSingleBubble(badgeName, NotificationBubbleType.BADGE_RECEIVED, badgeImage, parser.badgeCode, senderName);
@@ -392,8 +374,7 @@ const useNotificationStore = () =>
{
const parser = event.getParser();
// Skip badge notifications — handled by BadgeReceivedEvent with "Wear" button
if(parser.type === 'badge_received' || parser.type === 'badges' || parser.type.includes('badge')) return;
if(parser.type === 'badge_received' || parser.type === 'badges') return;
showNotification(parser.type, parser.parameters);
});
@@ -512,14 +493,6 @@ const useNotificationStore = () =>
return { alerts, bubbleAlerts, confirms, simpleAlert, showNitroAlert, showTradeAlert, showConfirm, showSingleBubble, closeAlert, closeBubbleAlert, closeConfirm };
};
/**
* 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);
@@ -527,14 +500,6 @@ export const useNotificationState = () =>
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 {
@@ -560,10 +525,4 @@ export const useNotificationActions = () =>
};
};
/**
* @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);