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
Original file line number Diff line number Diff line change
Expand Up @@ -2,98 +2,113 @@ import {
fetchWaveMessages,
fetchAroundSerialNoWaveMessages,
fetchLightWaveMessages,
} from '@/contexts/wave/utils/wave-messages-utils';
import { commonApiFetch, commonApiFetchWithRetry } from '@/services/api/common-api';
} from "@/contexts/wave/utils/wave-messages-utils";
import {
commonApiFetch,
commonApiFetchWithRetry,
} from "@/services/api/common-api";

jest.mock('@/services/api/common-api');
jest.mock("@/services/api/common-api");

const drop = { id: 'd1', serial_no: 1, created_at: '2020', wave: { id: 'w' } } as any;
const drop = {
id: "d1",
serial_no: 1,
created_at: "2020",
wave: { id: "w" },
} as any;

const mockFetch = commonApiFetch as jest.Mock;
const mockFetchRetry = commonApiFetchWithRetry as jest.Mock;

const makeBatch = (start: number, count: number) =>
Array.from({ length: count }, (_, index) => ({ serial_no: start - index }));
Array.from({ length: count }, (_, index) => {
const serialNo = start - index;
return { id: `id-${serialNo}`, serial_no: serialNo };
});

beforeEach(() => {
jest.clearAllMocks();
});

describe('wave-messages-utils additional', () => {
it('fetchWaveMessages returns mapped drops', async () => {
mockFetch.mockResolvedValue({ drops: [drop], wave: { id: 'w' } });
const res = await fetchWaveMessages('w', null);
expect(mockFetch).toHaveBeenCalledWith(expect.objectContaining({ endpoint: 'waves/w/drops' }));
expect(res?.[0]?.wave).toEqual({ id: 'w' });
describe("wave-messages-utils additional", () => {
it("fetchWaveMessages returns mapped drops", async () => {
mockFetch.mockResolvedValue({ drops: [drop], wave: { id: "w" } });
const res = await fetchWaveMessages("w", null);
expect(mockFetch).toHaveBeenCalledWith(
expect.objectContaining({ endpoint: "waves/w/drops" })
);
expect(res?.[0]?.wave).toEqual({ id: "w" });
});

it('fetchWaveMessages rethrows abort errors', async () => {
const err = new DOMException('aborted', 'AbortError');
it("fetchWaveMessages rethrows abort errors", async () => {
const err = new DOMException("aborted", "AbortError");
mockFetch.mockRejectedValue(err);
await expect(fetchWaveMessages('w', null)).rejects.toBe(err);
await expect(fetchWaveMessages("w", null)).rejects.toBe(err);
});

it('fetchAroundSerialNoWaveMessages uses retry fetch', async () => {
mockFetchRetry.mockResolvedValue({ drops: [drop], wave: { id: 'w' } });
const res = await fetchAroundSerialNoWaveMessages('w', 5);
it("fetchAroundSerialNoWaveMessages uses retry fetch", async () => {
mockFetchRetry.mockResolvedValue({ drops: [drop], wave: { id: "w" } });
const res = await fetchAroundSerialNoWaveMessages("w", 5);
expect(mockFetchRetry).toHaveBeenCalled();
expect(res?.[0]?.serial_no).toBe(1);
});

it('fetchLightWaveMessages gathers light drops across pages and merges full drops', async () => {
const lightBatches = [makeBatch(4005, 2000), makeBatch(2004, 2000)];
it("fetchLightWaveMessages gathers light drops across pages and merges full drops", async () => {
const lightBatches = [makeBatch(10000, 5000), makeBatch(5001, 4997)];
let lightCallCount = 0;
mockFetchRetry.mockImplementation(async (options) => {
if (options.endpoint === 'light-drops') {
if (options.endpoint === "drop-ids") {
const batch = lightBatches[lightCallCount++] ?? [];
return batch;
}

if (options.endpoint === 'waves/w/drops') {
if (options.endpoint === "waves/w/drops") {
return {
drops: [
{
serial_no: 5,
id: 'full-5',
created_at: '2020-01-01',
wave: { id: 'w' },
id: "full-5",
created_at: "2020-01-01",
wave: { id: "w" },
},
],
wave: { id: 'w' },
wave: { id: "w" },
};
}

throw new Error(`Unexpected endpoint: ${options.endpoint}`);
});

const result = await fetchLightWaveMessages('w', 4005, 5);
const result = await fetchLightWaveMessages("w", 4005, 5);

expect(lightCallCount).toBe(2);
expect(mockFetchRetry).toHaveBeenCalledTimes(3);
expect(mockFetchRetry).toHaveBeenCalledWith(
expect.objectContaining({ endpoint: 'light-drops' })
expect.objectContaining({ endpoint: "drop-ids" })
);
expect(result).not.toBeNull();
expect(result!.length).toBe(4000);
expect(result![0]?.serial_no).toBe(4005);
expect(result!.length).toBe(9996);
expect(result![0]?.serial_no).toBe(10000);
expect(result![result!.length - 1]?.serial_no).toBe(5);
expect(result!.find((d) => d.serial_no === 5)).toMatchObject({ id: 'full-5' });
expect(result!.find((d) => d.serial_no === 5)).toMatchObject({
id: "full-5",
});
});

it('fetchLightWaveMessages returns null when target serial is not found', async () => {
it("fetchLightWaveMessages returns null when target serial is not found", async () => {
mockFetchRetry.mockImplementation(async (options) => {
if (options.endpoint === 'light-drops') {
if (options.endpoint === "drop-ids") {
return [];
}

if (options.endpoint === 'waves/w/drops') {
return { drops: [], wave: { id: 'w' } };
if (options.endpoint === "waves/w/drops") {
return { drops: [], wave: { id: "w" } };
}

throw new Error(`Unexpected endpoint: ${options.endpoint}`);
});

const result = await fetchLightWaveMessages('w', 10, 5);
const result = await fetchLightWaveMessages("w", 10, 5);

expect(result).toBeNull();
});
Expand Down
2 changes: 2 additions & 0 deletions components/waves/CreateDropContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -411,10 +411,12 @@ const getOptimisticDrop = (
: null,
replies_count: 0,
quotes_count: 0,
mentioned_waves: [],
})),
parts_count: dropRequest.parts.length,
referenced_nfts: dropRequest.referenced_nfts,
mentioned_users: dropRequest.mentioned_users,

metadata: dropRequest.metadata,
rating: 0,
top_raters: [],
Expand Down
22 changes: 11 additions & 11 deletions components/waves/drops/LightDrop.tsx
Original file line number Diff line number Diff line change
@@ -1,27 +1,27 @@
import type { FC } from "react";
import type { ApiLightDrop } from "@/generated/models/ApiLightDrop";
import type { LightDropSummary } from "@/helpers/waves/drop.helpers";

interface LightDropProps {
readonly drop: ApiLightDrop;
readonly drop: LightDropSummary;
}

const LightDrop: FC<LightDropProps> = () => {
return (
<div className="tw-flex tw-flex-col tw-w-full tw-p-3 tw-gap-2 tw-border-b tw-border-iron-800">
<div className="tw-flex tw-w-full tw-flex-col tw-gap-2 tw-border-b tw-border-iron-800 tw-p-3">
<div className="tw-flex tw-items-center tw-gap-2">
<div className="tw-w-8 tw-h-8 tw-rounded-full tw-bg-iron-800" />
<div className="tw-h-8 tw-w-8 tw-rounded-full tw-bg-iron-800" />
<div className="tw-flex tw-flex-col tw-gap-1">
<div className="tw-w-24 tw-h-3 tw-rounded tw-bg-iron-800" />
<div className="tw-w-16 tw-h-2 tw-rounded tw-bg-iron-800" />
<div className="tw-h-3 tw-w-24 tw-rounded tw-bg-iron-800" />
<div className="tw-h-2 tw-w-16 tw-rounded tw-bg-iron-800" />
</div>
</div>
<div className="tw-flex tw-flex-col tw-gap-1">
<div className="tw-w-full tw-h-3 tw-rounded tw-bg-iron-800" />
<div className="tw-w-3/4 tw-h-3 tw-rounded tw-bg-iron-800" />
<div className="tw-h-3 tw-w-full tw-rounded tw-bg-iron-800" />
<div className="tw-h-3 tw-w-3/4 tw-rounded tw-bg-iron-800" />
</div>
<div className="tw-flex tw-justify-between tw-mt-1">
<div className="tw-w-20 tw-h-2 tw-rounded tw-bg-iron-800" />
<div className="tw-w-12 tw-h-2 tw-rounded tw-bg-iron-800" />
<div className="tw-mt-1 tw-flex tw-justify-between">
<div className="tw-h-2 tw-w-20 tw-rounded tw-bg-iron-800" />
<div className="tw-h-2 tw-w-12 tw-rounded tw-bg-iron-800" />
</div>
</div>
);
Expand Down
22 changes: 7 additions & 15 deletions contexts/wave/MyStreamContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,11 @@

import { useNotificationsContext } from "@/components/notifications/NotificationsContext";
import type { ApiDrop } from "@/generated/models/ApiDrop";
import type { ApiLightDrop } from "@/generated/models/ApiLightDrop";
import type { ApiDropId } from "@/generated/models/ApiDropId";
import type { Drop } from "@/helpers/waves/drop.helpers";
import useCapacitor from "@/hooks/useCapacitor";
import { useWebsocketStatus } from "@/services/websocket/useWebSocketMessage";
import type {
ReactNode} from "react";
import type { ReactNode } from "react";
import React, {
createContext,
useCallback,
Expand All @@ -20,21 +19,14 @@ import React, {
import type { WaveMessages } from "./hooks/types";
import { useActiveWaveManager } from "./hooks/useActiveWaveManager";
import useEnhancedDmWavesList from "./hooks/useEnhancedDmWavesList";
import type {
MinimalWave,
} from "./hooks/useEnhancedWavesList";
import type { MinimalWave } from "./hooks/useEnhancedWavesList";
import useEnhancedWavesList from "./hooks/useEnhancedWavesList";
import { useWaveDataManager } from "./hooks/useWaveDataManager";
import type {
Listener as WaveMessagesListener,
} from "./hooks/useWaveMessagesStore";
import type { Listener as WaveMessagesListener } from "./hooks/useWaveMessagesStore";
import useWaveMessagesStore from "./hooks/useWaveMessagesStore";
import type { NextPageProps } from "./hooks/useWavePagination";
import type {
ProcessIncomingDropType} from "./hooks/useWaveRealtimeUpdater";
import {
useWaveRealtimeUpdater,
} from "./hooks/useWaveRealtimeUpdater";
import type { ProcessIncomingDropType } from "./hooks/useWaveRealtimeUpdater";
import { useWaveRealtimeUpdater } from "./hooks/useWaveRealtimeUpdater";

// Define nested structures for context data
interface WavesContextData {
Expand Down Expand Up @@ -77,7 +69,7 @@ interface MyStreamContextType {
readonly registerWave: (waveId: string, syncNewest?: boolean) => void;
readonly fetchNextPageForWave: (
props: NextPageProps
) => Promise<(ApiDrop | ApiLightDrop)[] | null>;
) => Promise<(ApiDrop | ApiDropId)[] | null>;
readonly fetchAroundSerialNo: (waveId: string, serialNo: number) => void;
readonly processIncomingDrop: (
drop: ApiDrop,
Expand Down
15 changes: 8 additions & 7 deletions contexts/wave/hooks/useWavePagination.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ import {
fetchAroundSerialNoWaveMessages,
} from "../utils/wave-messages-utils";
import { DropSize } from "@/helpers/waves/drop.helpers";
import type { ApiLightDrop } from "@/generated/models/ApiLightDrop";
import type { ApiDropId } from "@/generated/models/ApiDropId";
import { WAVE_DROPS_PARAMS } from "@/components/react-query-wrapper/utils/query-utils";

// Tracks which waves are currently loading next page
interface PaginationState {
isLoading: boolean;
promise: Promise<(ApiDrop | ApiLightDrop)[] | null> | null;
promise: Promise<(ApiDrop | ApiDropId)[] | null> | null;
}

export type NextPageProps = NextPageFullProps | NextPageLightProps;
Expand Down Expand Up @@ -130,7 +130,7 @@ export function useWavePagination({
* Updates store with new paginated data
*/
const updateWithPaginatedData = useCallback(
(waveId: string, newDrops: (ApiDrop | ApiLightDrop)[] | null) => {
(waveId: string, newDrops: (ApiDrop | ApiDropId)[] | null) => {
// Clear the loading state
if (paginationStates.current[waveId]) {
paginationStates.current[waveId].isLoading = false;
Expand Down Expand Up @@ -159,12 +159,15 @@ export function useWavePagination({
return null;
}

const isFullDrop = (drop: ApiDrop | ApiDropId): drop is ApiDrop =>
"wave" in drop;

updateData({
key: waveId,
isLoadingNextPage: false,
hasNextPage: newDrops.length > 0,
drops: newDrops.map((drop) => {
if ("part_1_text" in drop) {
if (!isFullDrop(drop)) {
return {
...drop,
waveId,
Expand Down Expand Up @@ -225,9 +228,7 @@ export function useWavePagination({
* Fetches the next page of data for a wave
*/
const fetchNextPage = useCallback(
async (
props: NextPageProps
): Promise<(ApiDrop | ApiLightDrop)[] | null> => {
async (props: NextPageProps): Promise<(ApiDrop | ApiDropId)[] | null> => {
// Get current state
const currentData = getData(props.waveId);
if (!currentData) {
Expand Down
Loading