+
diff --git a/hooks/useWaveDropsLeaderboard.ts b/hooks/useWaveDropsLeaderboard.ts
index 438dc7f75a..8a35db3ac7 100644
--- a/hooks/useWaveDropsLeaderboard.ts
+++ b/hooks/useWaveDropsLeaderboard.ts
@@ -1,7 +1,6 @@
"use client";
-import { useCallback, useEffect, useState, useMemo } from "react";
-import type { ExtendedDrop } from "@/helpers/waves/drop.helpers";
+import { useCallback, useEffect, useState, useMemo, useRef } from "react";
import {
useInfiniteQuery,
useQuery,
@@ -24,6 +23,7 @@ import { QueryKey } from "@/components/react-query-wrapper/ReactQueryWrapper";
export enum WaveDropsLeaderboardSort {
RANK = "RANK",
RATING_PREDICTION = "RATING_PREDICTION",
+ TREND = "TREND",
MY_REALTIME_VOTE = "MY_REALTIME_VOTE",
CREATED_AT = "CREATED_AT",
}
@@ -35,13 +35,16 @@ interface UseWaveDropsLeaderboardProps {
readonly pausePolling?: boolean | undefined;
}
-const SORT_DIRECTION_MAP: Record
=
- {
- [WaveDropsLeaderboardSort.RANK]: undefined,
- [WaveDropsLeaderboardSort.RATING_PREDICTION]: "DESC",
- [WaveDropsLeaderboardSort.MY_REALTIME_VOTE]: undefined,
- [WaveDropsLeaderboardSort.CREATED_AT]: "DESC",
- };
+const SORT_DIRECTION_MAP: Record<
+ WaveDropsLeaderboardSort,
+ "ASC" | "DESC" | undefined
+> = {
+ [WaveDropsLeaderboardSort.RANK]: undefined,
+ [WaveDropsLeaderboardSort.RATING_PREDICTION]: "DESC",
+ [WaveDropsLeaderboardSort.TREND]: "DESC",
+ [WaveDropsLeaderboardSort.MY_REALTIME_VOTE]: undefined,
+ [WaveDropsLeaderboardSort.CREATED_AT]: "DESC",
+};
const POLLING_DELAY = 3000;
const ACTIVE_POLLING_INTERVAL = 5000;
@@ -69,26 +72,9 @@ export function useWaveDropsLeaderboard({
const { isCapacitor } = useCapacitor();
const queryClient = useQueryClient();
- const [drops, setDrops] = useState([]);
- const [hasInitialized, setHasInitialized] = useState(false);
- const [haveNewDrops, setHaveNewDrops] = useState(false);
const [canPoll, setCanPoll] = useState(false);
- const [delayedPollingResult, setDelayedPollingResult] = useState<
- ApiDropsLeaderboardPage | undefined
- >(undefined);
const isTabVisible = useTabVisibility();
- const [currentSort, setCurrentSort] = useState(sort);
-
- // Detect sort changes
- const isSortChanging = currentSort !== sort;
-
- useEffect(() => {
- if (currentSort !== sort) {
- setCurrentSort(sort);
- setDrops([]);
- setHasInitialized(false);
- }
- }, [sort, currentSort]);
+ const pollingTimeoutRef = useRef | null>(null);
const sortDirection = SORT_DIRECTION_MAP[sort];
@@ -130,7 +116,7 @@ export function useWaveDropsLeaderboard({
params["sort_direction"] = sortDirection;
}
- if (pageParam) {
+ if (typeof pageParam === "number") {
params["page"] = `${pageParam}`;
}
@@ -166,16 +152,14 @@ export function useWaveDropsLeaderboard({
params["sort_direction"] = sortDirection;
}
- if (pageParam) {
+ if (typeof pageParam === "number") {
params["page"] = `${pageParam}`;
}
- const results = await commonApiFetch({
+ return await commonApiFetch({
endpoint: `waves/${waveId}/leaderboard`,
params,
});
-
- return results;
},
initialPageParam: null,
getNextPageParam,
@@ -184,7 +168,8 @@ export function useWaveDropsLeaderboard({
...getDefaultQueryRetry(),
});
- const processedDrops = useMemo(() => {
+ // Derive drops directly during render - no need for state
+ const drops = useMemo(() => {
if (!data?.pages) return [];
const mappedDrops = mapToExtendedDrops(
@@ -206,35 +191,76 @@ export function useWaveDropsLeaderboard({
return uniqueDrops;
}, [data, sort]);
- useEffect(() => {
- if (!data?.pages) {
- return;
- }
-
- setDrops(processedDrops);
- setHasInitialized(true);
- }, [processedDrops, data]);
+ // Derive hasInitialized from whether we have data
+ const hasInitialized = !!data?.pages;
useDebounce(() => setCanPoll(true), 10000, [data]);
- const { data: pollingResult } = useQuery({
+ // Check if we can auto-refetch (derived during render)
+ const hasTempDrop = useMemo(
+ () => drops.some((drop) => drop.id.startsWith("temp-")),
+ [drops]
+ );
+ const canAutoRefetch = isTabVisible && !hasTempDrop;
+
+ // Helper to check if polling result has newer data than current drops
+ // Note: We compare against the max created_at across ALL loaded drops,
+ // not just drops[0], because drops may be sorted by RANK/TREND (not time)
+ const checkForNewDrops = useCallback(
+ (pollingData: ApiDropsLeaderboardPage): boolean => {
+ if (pollingData.drops.length === 0 || drops.length === 0) return false;
+
+ const latestPolledDrop = pollingData.drops[0];
+
+ // Find the actual newest drop by created_at across all loaded drops
+ const newestExistingTimestamp = Math.max(
+ ...drops.map((drop) => new Date(drop.created_at).getTime())
+ );
+
+ if (latestPolledDrop) {
+ const polledCreatedAt = new Date(latestPolledDrop.created_at).getTime();
+ return polledCreatedAt > newestExistingTimestamp;
+ }
+
+ return true;
+ },
+ [drops]
+ );
+
+ // Polling query with select to determine if there are new drops
+ // Uses select to derive haveNewDrops directly from query data
+ // Always uses CREATED_AT sort to detect genuinely new drops regardless of main query's sort
+ const { data: haveNewDrops = false } = useQuery({
queryKey: [...queryKey, "polling"],
queryFn: async () => {
const params: Record = {
page_size: "1",
- sort: sort,
+ sort: WaveDropsLeaderboardSort.CREATED_AT,
+ sort_direction: "DESC",
};
- if (sortDirection) {
- params["sort_direction"] = sortDirection;
- }
-
- return await commonApiFetch({
+ const result = await commonApiFetch({
endpoint: `waves/${waveId}/leaderboard`,
params,
});
+
+ // Trigger refetch directly in the query callback when conditions are met
+ // This replaces the effect-based approach
+ if (canAutoRefetch && checkForNewDrops(result)) {
+ // Clear any existing timeout to prevent accumulation
+ if (pollingTimeoutRef.current) {
+ clearTimeout(pollingTimeoutRef.current);
+ }
+ // Use setTimeout to defer the refetch slightly and avoid React Query batching issues
+ pollingTimeoutRef.current = setTimeout(() => {
+ refetch();
+ }, POLLING_DELAY);
+ }
+
+ return result;
},
- enabled: !haveNewDrops && canPoll && !pausePolling,
+ select: checkForNewDrops,
+ enabled: canPoll && !pausePolling,
refetchInterval: isTabVisible
? ACTIVE_POLLING_INTERVAL
: INACTIVE_POLLING_INTERVAL,
@@ -245,53 +271,12 @@ export function useWaveDropsLeaderboard({
...getDefaultQueryRetry(),
});
- useEffect(() => {
- if (pollingResult && !pausePolling) {
- const timer = setTimeout(() => {
- setDelayedPollingResult(pollingResult);
- }, POLLING_DELAY);
-
- return () => clearTimeout(timer);
- }
- return;
- }, [pollingResult, pausePolling]);
-
- useEffect(() => {
- if (delayedPollingResult !== undefined) {
- if (delayedPollingResult.drops.length > 0) {
- const latestPolledDrop = delayedPollingResult.drops[0];
-
- if (drops.length > 0) {
- const latestExistingDrop = drops.at(-1);
-
- const polledCreatedAt = new Date(
- latestPolledDrop?.created_at!
- ).getTime();
- const existingCreatedAt = new Date(
- latestExistingDrop?.created_at ?? 0
- ).getTime();
-
- setHaveNewDrops(polledCreatedAt > existingCreatedAt);
- } else {
- setHaveNewDrops(true);
- }
- } else {
- setHaveNewDrops(false);
- }
- }
- }, [delayedPollingResult, drops]);
-
- useEffect(() => {
- if (!haveNewDrops) return;
- if (!isTabVisible) return;
- const hasTempDrop = drops.some((drop) => drop.id.startsWith("temp-"));
- if (hasTempDrop) return;
- refetch();
- setHaveNewDrops(false);
- }, [haveNewDrops, isTabVisible, drops]);
-
useEffect(() => {
return () => {
+ // Clear polling timeout on unmount to prevent stale refetch calls
+ if (pollingTimeoutRef.current) {
+ clearTimeout(pollingTimeoutRef.current);
+ }
queryClient.removeQueries({
queryKey: [QueryKey.DROPS, { waveId }],
});
@@ -308,7 +293,7 @@ export function useWaveDropsLeaderboard({
drops,
fetchNextPage,
hasNextPage,
- isFetching: isFetching || !hasInitialized || isSortChanging,
+ isFetching: isFetching || !hasInitialized,
isFetchingNextPage,
refetch,
haveNewDrops,
diff --git a/openapi.yaml b/openapi.yaml
index 36c09dd5e8..8af135b1f6 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -5814,37 +5814,6 @@ components:
type: number
value_count:
type: number
- ApiMintMetrics:
- type: object
- required:
- - card
- - mint_time
- - subscriptions
- - mints
- properties:
- card:
- type: number
- format: int64
- mint_time:
- type: number
- format: int64
- subscriptions:
- type: number
- format: int64
- mints:
- type: number
- format: int64
- ApiMintMetricsPage:
- type: object
- required:
- - data
- allOf:
- - $ref: "#/components/schemas/ApiPageBase"
- properties:
- data:
- type: array
- items:
- $ref: "#/components/schemas/ApiMintMetrics"
ApiCompleteMultipartUploadRequest:
required:
- upload_id
diff --git a/scripts/worktree/copy.conf b/scripts/worktree/copy.conf
index c1095bd974..72ebeb7926 100644
--- a/scripts/worktree/copy.conf
+++ b/scripts/worktree/copy.conf
@@ -8,3 +8,4 @@
# Environment files
.env.development
+.env.production