mirror of
https://github.com/duckietm/Nitro-V3.git
synced 2026-06-19 23:16:21 +00:00
🆕 Redesign of HC Club buy, now also give as gift
This commit is contained in:
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user