Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 41 additions & 30 deletions __tests__/hooks/useWavesList.test.tsx
Original file line number Diff line number Diff line change
@@ -1,50 +1,56 @@
import { renderHook } from '@testing-library/react';
import React from 'react';
import useWavesList from '@/hooks/useWavesList';
import { AuthContext } from '@/components/auth/Auth';
import { ApiWaveType } from '@/generated/models/ApiWaveType';
import { renderHook } from "@testing-library/react";
import React from "react";
import useWavesList from "@/hooks/useWavesList";
import { AuthContext } from "@/components/auth/Auth";
import { ApiWaveType } from "@/generated/models/ApiWaveType";

jest.mock('@/hooks/useWavesOverview', () => ({
jest.mock("@/hooks/useWavesOverview", () => ({
useWavesOverview: jest.fn(),
}));

jest.mock('@/hooks/usePinnedWavesServer', () => ({
jest.mock("@/hooks/usePinnedWavesServer", () => ({
usePinnedWavesServer: jest.fn(),
}));

jest.mock('@/hooks/useWaveData', () => ({
jest.mock("@/hooks/useWaveData", () => ({
useWaveData: jest.fn(),
}));

jest.mock('@/hooks/useShowFollowingWaves', () => ({
jest.mock("@/hooks/useShowFollowingWaves", () => ({
useShowFollowingWaves: jest.fn(() => [false]),
}));

const useWavesOverviewMock = require('@/hooks/useWavesOverview').useWavesOverview as jest.Mock;
const usePinnedWavesServerMock = require('@/hooks/usePinnedWavesServer').usePinnedWavesServer as jest.Mock;
const useWaveDataMock = require('@/hooks/useWaveData').useWaveData as jest.Mock;
const useWavesOverviewMock = require("@/hooks/useWavesOverview")
.useWavesOverview as jest.Mock;
const usePinnedWavesServerMock = require("@/hooks/usePinnedWavesServer")
.usePinnedWavesServer as jest.Mock;
const useWaveDataMock = require("@/hooks/useWaveData").useWaveData as jest.Mock;

const wrapper: React.FC<{ children: React.ReactNode }> = ({ children }) => (
<AuthContext.Provider value={{ connectedProfile: { handle: 'me' }, activeProfileProxy: null } as any}>
<AuthContext.Provider
value={
{ connectedProfile: { handle: "me" }, activeProfileProxy: null } as any
}
>
{children}
</AuthContext.Provider>
);

const dmWave = {
id: '1',
id: "1",
created_at: 0,
metrics: { latest_drop_timestamp: 50 },
wave: { type: ApiWaveType.Chat },
chat: { scope: { group: { is_direct_message: true } } },
} as any;
const mainWave = {
id: '2',
id: "2",
created_at: 1,
metrics: { latest_drop_timestamp: 100 },
wave: { type: ApiWaveType.Rank },
} as any;
const pinnedExtra = {
id: '3',
id: "3",
created_at: 2,
metrics: { latest_drop_timestamp: 200 },
wave: { type: ApiWaveType.Rank },
Expand All @@ -58,38 +64,43 @@ beforeEach(() => {
isFetchingNextPage: false,
hasNextPage: false,
fetchNextPage: jest.fn(),
status: 'success',
status: "success",
refetch: jest.fn(),
});
usePinnedWavesServerMock.mockReturnValue({
pinnedIds: ['2', '3'],
usePinnedWavesServerMock.mockReturnValue({
pinnedIds: ["2", "3"],
pinnedWaves: [pinnedExtra],
pinWave: jest.fn(),
pinWave: jest.fn(),
unpinWave: jest.fn(),
isLoading: false,
isError: false,
refetch: jest.fn()
refetch: jest.fn(),
});
useWaveDataMock.mockReturnValue({
data: pinnedExtra,
isLoading: false,
isError: false,
refetch: jest.fn(),
});
useWaveDataMock.mockReturnValue({ data: pinnedExtra, isLoading: false, isError: false, refetch: jest.fn() });
});

test('combines main and pinned waves, filtering DMs and flagging pinned', () => {
test("combines main and pinned waves, filtering DMs and flagging pinned", () => {
const { result } = renderHook(() => useWavesList(), { wrapper });
const waves = result.current.waves;
expect(waves.map((w: any) => w.id)).toEqual(['3', '2']);
expect(waves.map((w: any) => w.id)).toEqual(["3", "2"]);
expect(waves.every((w: any) => w.isPinned)).toBe(true);
expect(result.current.pinnedWaves.map((w: any) => w.id)).toEqual(['3']);
expect(result.current.pinnedWaves.map((w: any) => w.id)).toEqual(["3"]);
});

test('indicates loading when pinned wave is still loading', () => {
usePinnedWavesServerMock.mockReturnValue({
pinnedIds: ['2', '3'],
test("indicates loading when pinned wave is still loading", () => {
usePinnedWavesServerMock.mockReturnValue({
pinnedIds: ["2", "3"],
pinnedWaves: [],
pinWave: jest.fn(),
pinWave: jest.fn(),
unpinWave: jest.fn(),
isLoading: true,
isError: false,
refetch: jest.fn()
refetch: jest.fn(),
});
const { result } = renderHook(() => useWavesList(), { wrapper });
expect(result.current.isPinnedWavesLoading).toBe(true);
Expand Down
9 changes: 6 additions & 3 deletions hooks/usePinnedWavesServer.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"use client";

import { useCallback, useContext, useEffect, useRef } from "react";
import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
import {
useQuery,
useMutation,
Expand Down Expand Up @@ -78,8 +78,11 @@ export function usePinnedWavesServer(): UsePinnedWavesServerReturn {
}
}, [isAuthenticated, PINNED_WAVES_QUERY_KEY, queryClient]);

// Derive pinned IDs from pinned waves
const pinnedIds = pinnedWaves.map((wave) => wave.id);
// Derive pinned IDs from pinned waves with stable identity
const pinnedIds = useMemo(
() => pinnedWaves.map((wave) => wave.id),
[pinnedWaves]
);

// Shared invalidation logic for both pin and unpin operations
const invalidateWavesQueries = useCallback(() => {
Expand Down
61 changes: 9 additions & 52 deletions hooks/useWavesList.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
"use client";

import {
useContext,
useMemo,
useCallback,
useRef,
useEffect,
useState,
} from "react";
import { useContext, useMemo, useCallback } from "react";
import { AuthContext } from "@/components/auth/Auth";
import { useWavesOverview } from "./useWavesOverview";
import { WAVE_FOLLOWING_WAVES_PARAMS } from "@/components/react-query-wrapper/utils/query-utils";
Expand All @@ -21,22 +14,6 @@ interface EnhancedWave extends ApiWave {
isPinned: boolean;
}

// Helper function for deep comparison of wave arrays
function areWavesEqual(arrA: EnhancedWave[], arrB: EnhancedWave[]): boolean {
if (arrA === arrB) return true;
if (arrA.length !== arrB.length) return false;

// Compare each wave by ID, updatedAt, isPinned, and muted status
for (let i = 0; i < arrA.length; i++) {
if (arrA[i]?.id !== arrB[i]?.id) return false;
if (arrA[i]?.created_at !== arrB[i]?.created_at) return false;
if (arrA[i]?.isPinned !== arrB[i]?.isPinned) return false;
if (arrA[i]?.metrics.muted !== arrB[i]?.metrics.muted) return false;
}

return true;
}

/**
* Hook for managing and fetching waves list including pinned waves
* @returns Wave list data and loading states
Expand All @@ -53,12 +30,6 @@ const useWavesList = () => {
refetch: refetchPinnedWaves,
} = usePinnedWavesServer();
const [following] = useShowFollowingWaves();
// Use state for allWaves instead of ref to ensure reactivity
const [allWaves, setAllWaves] = useState<EnhancedWave[]>([]);

// Use ref to avoid too many re-renders for derived values
const prevMainWavesRef = useRef<ApiWave[]>([]);
const prevPinnedWavesRef = useRef<EnhancedWave[]>([]);

// Track connected identity state - memoize to prevent re-renders
const isConnectedIdentity = useMemo(() => {
Expand All @@ -84,7 +55,6 @@ const useWavesList = () => {
const mainWavesMap = useMemo(() => {
const map = new Map<string, ApiWave>();
mainWaves.forEach((wave) => map.set(wave.id, wave));
prevMainWavesRef.current = mainWaves;
return map;
}, [mainWaves]);

Expand All @@ -94,7 +64,7 @@ const useWavesList = () => {
}, [mainWavesMap]);

// Server provides full pinned waves data, so no individual fetching needed
const missingPinnedIds: string[] = [];
const missingPinnedIds = useMemo<string[]>(() => [], []);
// Function to refetch all waves (main and pinned)
const refetchAllWaves = useCallback(() => {
// Refetch main waves overview
Expand All @@ -120,12 +90,7 @@ const useWavesList = () => {
}
});

// Update the ref if content changed - still useful for comparison and memoization
if (!areWavesEqual(result, prevPinnedWavesRef.current)) {
prevPinnedWavesRef.current = result;
}

return result; // Return the actual result, not the ref
return result;
}, [serverPinnedWaves]);

// New drops counts are now managed externally
Expand Down Expand Up @@ -179,18 +144,10 @@ const useWavesList = () => {
return allWavesArray;
}, [mainWaves, separatelyFetchedPinnedWaves, pinnedIds]);

// New drops counting logic has been removed and will be managed by context
// Derived data should come directly from memoized inputs
const allWaves = combinedWaves;

// Update allWaves state when the combined waves change
useEffect(() => {
// Using a functional update and removing the allWaves dependency
// to break the infinite update cycle
setAllWaves((prevWaves) => {
return !areWavesEqual(combinedWaves, prevWaves)
? combinedWaves
: prevWaves;
});
}, [combinedWaves]);
// New drops counting logic has been removed and will be managed by context

// Use server-side loading and error states
const isPinnedWavesLoading = isPinnedWavesLoadingServer;
Expand All @@ -205,7 +162,7 @@ const useWavesList = () => {
// Components using this hook will only re-render when the values they use actually change
return useMemo(
() => ({
// Main data - now using state instead of ref, with enhanced type
// Main data - derived from combined waves, with enhanced type
waves: allWaves,

// Original waves pagination and loading
Expand All @@ -225,7 +182,7 @@ const useWavesList = () => {
removePinnedWave: unpinWave,

// Additional data that might be useful
mainWaves: prevMainWavesRef.current,
mainWaves,
missingPinnedIds,
mainWavesRefetch,
// Refetch all waves including main and pinned
Expand All @@ -238,12 +195,12 @@ const useWavesList = () => {
hasNextPage,
fetchNextPageStable,
mainWavesStatus,
mainWaves,
allPinnedWaves,
isPinnedWavesLoading,
hasPinnedWavesError,
pinWave,
unpinWave,
prevMainWavesRef.current,
missingPinnedIds,
mainWavesRefetch,
refetchAllWaves,
Expand Down