diff --git a/src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx b/src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx index 88cdd490f40..a9c4b973d74 100644 --- a/src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx +++ b/src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx @@ -5,56 +5,73 @@ import { Image, Screen, Spacer, + Spinner, Text, Touchable, useScreenDimensions, useTheme, } from "@artsy/palette-mobile" -import { InfiniteDiscoveryRefetchQuery } from "__generated__/InfiniteDiscoveryRefetchQuery.graphql" -import { InfiniteDiscovery_Fragment$key } from "__generated__/InfiniteDiscovery_Fragment.graphql" import { FancySwiper } from "app/Components/FancySwiper/FancySwiper" import { InfiniteDiscoveryBottomSheet } from "app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryBottomSheet" +import { GlobalStore } from "app/store/GlobalStore" import { goBack } from "app/system/navigation/navigate" +import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { extractNodes } from "app/utils/extractNodes" -import { NoFallback, SpinnerFallback, withSuspense } from "app/utils/hooks/withSuspense" import { sizeToFit } from "app/utils/useSizeToFit" import { useEffect, useState } from "react" -import { graphql, useLazyLoadQuery, useRefetchableFragment } from "react-relay" +import { fetchQuery, graphql } from "react-relay" import type { InfiniteDiscoveryQuery, InfiniteDiscoveryQuery$data, } from "__generated__/InfiniteDiscoveryQuery.graphql" import type { Card } from "app/Components/FancySwiper/FancySwiperCard" -interface InfiniteDiscoveryProps { - artworks: InfiniteDiscoveryQuery$data -} +type Artwork = NonNullable< + NonNullable< + NonNullable["edges"]>[0] + >["node"] +> -export const InfiniteDiscovery: React.FC = ({ artworks: _artworks }) => { - const [data, refetch] = useRefetchableFragment< - InfiniteDiscoveryRefetchQuery, - InfiniteDiscovery_Fragment$key - >(infiniteDiscoveryFragment, _artworks) +export const InfiniteDiscovery: React.FC = () => { + const REFETCH_BUFFER = 3 - const REFETCH_BUFFER = 2 + const discoveredArtworksIds = GlobalStore.useAppState( + (state) => state.infiniteDiscovery.discoveredArtworkIds + ) + const { addDiscoveredArtworkId } = GlobalStore.actions.infiniteDiscovery const { color } = useTheme() const { width: screenWidth } = useScreenDimensions() const [index, setIndex] = useState(0) - const [artworks, setArtworks] = useState(extractNodes(data.discoverArtworks)) + const [artworks, setArtworks] = useState([]) useEffect(() => { - setArtworks((previousArtworks) => { - // only add new artworks to the list by filtering-out existing artworks - const newArtworks = extractNodes(data.discoverArtworks).filter( - (newArtwork) => - !previousArtworks.some((artwork) => artwork.internalID === newArtwork.internalID) - ) - - return [...previousArtworks, ...newArtworks] + fetchQuery( + getRelayEnvironment(), + infiniteDiscoveryQuery, + { excludeArtworkIds: discoveredArtworksIds }, + { + fetchPolicy: "network-only", + } + ).subscribe({ + next: (data) => { + if (!data) { + console.error("Error fetching infinite discovery batch: response is falsy") + return + } + + setArtworks(extractNodes(data.discoverArtworks)) + }, + error: (error: Error) => { + console.error("Error fetching infinite discovery batch:", error) + }, }) - }, [data, extractNodes, setArtworks]) + }, []) + + if (!artworks || artworks.length === 0) { + return + } const goToPrevious = () => { if (index > 0) { @@ -64,17 +81,35 @@ export const InfiniteDiscovery: React.FC = ({ artworks: const goToNext = () => { if (index < artworks.length - 1) { + addDiscoveredArtworkId(artworks[index].internalID) setIndex(index + 1) } // fetch more artworks when the user is about to reach the end of the list if (index === artworks.length - REFETCH_BUFFER) { - refetch( - { excludeArtworkIds: artworks.map((artwork) => artwork.internalID) }, + fetchQuery( + getRelayEnvironment(), + infiniteDiscoveryQuery, + { excludeArtworkIds: discoveredArtworksIds }, { fetchPolicy: "network-only", } - ) + ).subscribe({ + next: (data) => { + if (!data) { + console.error("Error fetching infinite discovery batch: response is falsy") + return + } + + setArtworks((previousArtworks) => { + const newArtworks = extractNodes(data.discoverArtworks) + return [...previousArtworks, ...newArtworks] + }) + }, + error: (error: Error) => { + console.error("Error fetching infinite discovery batch:", error) + }, + }) } } @@ -173,24 +208,21 @@ export const InfiniteDiscovery: React.FC = ({ artworks: ) } -export const InfiniteDiscoveryQueryRenderer = withSuspense({ - Component: () => { - const initialData = useLazyLoadQuery(infiniteDiscoveryQuery, {}) - - if (!initialData) { - return null - } +const InfiniteDiscoverySpinner: React.FC = () => ( + + + + + + + + +) - return - }, - LoadingFallback: SpinnerFallback, - ErrorFallback: NoFallback, -}) +export const InfiniteDiscoveryQueryRenderer = () => -const infiniteDiscoveryFragment = graphql` - fragment InfiniteDiscovery_Fragment on Query - @refetchable(queryName: "InfiniteDiscoveryRefetchQuery") - @argumentDefinitions(excludeArtworkIds: { type: "[String!]" }) { +export const infiniteDiscoveryQuery = graphql` + query InfiniteDiscoveryQuery($excludeArtworkIds: [String!]!) { discoverArtworks(excludeArtworkIds: $excludeArtworkIds) { edges { node { @@ -218,9 +250,3 @@ const infiniteDiscoveryFragment = graphql` } } ` - -export const infiniteDiscoveryQuery = graphql` - query InfiniteDiscoveryQuery { - ...InfiniteDiscovery_Fragment - } -` diff --git a/src/app/Scenes/InfiniteDiscovery/__tests__/InfiniteDiscovery.tests.tsx b/src/app/Scenes/InfiniteDiscovery/__tests__/InfiniteDiscovery.tests.tsx index a0f012528e6..bed43807ee3 100644 --- a/src/app/Scenes/InfiniteDiscovery/__tests__/InfiniteDiscovery.tests.tsx +++ b/src/app/Scenes/InfiniteDiscovery/__tests__/InfiniteDiscovery.tests.tsx @@ -1,83 +1,82 @@ import { fireEvent, screen } from "@testing-library/react-native" import { swipeLeft } from "app/Components/FancySwiper/__tests__/utils" -import { - infiniteDiscoveryQuery, - InfiniteDiscoveryQueryRenderer, -} from "app/Scenes/InfiniteDiscovery/InfiniteDiscovery" +import { InfiniteDiscovery } from "app/Scenes/InfiniteDiscovery/InfiniteDiscovery" import { goBack } from "app/system/navigation/navigate" -import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" +import { renderWithWrappers } from "app/utils/tests/renderWithWrappers" +import { fetchQuery } from "react-relay" +import { Observable } from "relay-runtime" jest.mock("app/system/navigation/navigate") +jest.mock("react-relay", () => ({ + ...jest.requireActual("react-relay"), + fetchQuery: jest.fn(), +})) jest.mock("app/Scenes/InfiniteDiscovery/Components/InfiniteDiscoveryBottomSheet", () => ({ InfiniteDiscoveryBottomSheet: () => null, })) describe("InfiniteDiscovery", () => { + const mockFetchQuery = fetchQuery as jest.MockedFunction + beforeEach(() => { jest.clearAllMocks() }) - it("hides the back button if the current artwork is on the first artwork", () => { - const { renderWithRelay } = setupTestWrapper({ - Component: InfiniteDiscoveryQueryRenderer, - query: infiniteDiscoveryQuery, - }) - renderWithRelay(marketingCollection) + it("hides the back button if the current artwork is on the first artwork", async () => { + await renderAndFetchFirstBatch(mockFetchQuery) + expect(screen.queryByText("Back")).not.toBeOnTheScreen() }) it("shows the back button if the current artwork is not the first artwork", async () => { - const { renderWithRelay } = setupTestWrapper({ - Component: InfiniteDiscoveryQueryRenderer, - query: infiniteDiscoveryQuery, - }) - renderWithRelay(marketingCollection) + await renderAndFetchFirstBatch(mockFetchQuery) + swipeLeft() + await screen.findByText("Back") }) it("returns to the previous artwork when the back button is pressed", async () => { - const { renderWithRelay } = setupTestWrapper({ - Component: InfiniteDiscoveryQueryRenderer, - query: infiniteDiscoveryQuery, - }) - renderWithRelay(marketingCollection) + await renderAndFetchFirstBatch(mockFetchQuery) + expect(screen.queryByText("Back")).not.toBeOnTheScreen() swipeLeft() + await screen.findByText("Back") + fireEvent.press(screen.getByText("Back")) expect(screen.queryByText("Back")).not.toBeOnTheScreen() }) - it("navigates to home view when the exit button is pressed", () => { - const { renderWithRelay } = setupTestWrapper({ - Component: InfiniteDiscoveryQueryRenderer, - query: infiniteDiscoveryQuery, - }) - renderWithRelay(marketingCollection) + it("navigates to home view when the exit button is pressed", async () => { + await renderAndFetchFirstBatch(mockFetchQuery) + fireEvent.press(screen.getByText("Exit")) expect(goBack).toHaveBeenCalledTimes(1) }) }) -const marketingCollection = { - ArtworkConnection: () => ({ - edges: [ - { - node: { - internalID: "artwork-1", - }, - }, - { - node: { - internalID: "artwork-2", - }, - }, - { - node: { - internalID: "artwork-3", +const renderAndFetchFirstBatch = async (mockFetchQuery: jest.MockedFunction) => { + mockFetchQuery.mockReturnValueOnce( + Observable.from( + Promise.resolve({ + discoverArtworks: { + edges: [ + { + node: { + internalID: "artwork-1", + }, + }, + { + node: { + internalID: "artwork-2", + }, + }, + ], }, - }, - ], - }), + }) + ) + ) + renderWithWrappers() + await screen.findByTestId("top-fancy-swiper-card") } diff --git a/src/app/store/GlobalStoreModel.ts b/src/app/store/GlobalStoreModel.ts index 480cd27a894..3f23bac989c 100644 --- a/src/app/store/GlobalStoreModel.ts +++ b/src/app/store/GlobalStoreModel.ts @@ -12,6 +12,7 @@ import { SubmissionModel, } from "app/Scenes/SellWithArtsy/utils/submissionModelState" import { unsafe__getEnvironment } from "app/store/GlobalStore" +import { getInfiniteDiscoveryModel, InfiniteDiscoveryModel } from "app/store/InfiniteDiscoveryModel" import { ProgressiveOnboardingModel, getProgressiveOnboardingModel, @@ -57,6 +58,7 @@ interface GlobalStoreStateModel { requestedPriceEstimates: RequestedPriceEstimatesModel recentPriceRanges: RecentPriceRangesModel progressiveOnboarding: ProgressiveOnboardingModel + infiniteDiscovery: InfiniteDiscoveryModel } export interface GlobalStoreModel extends GlobalStoreStateModel { rehydrate: Action>> @@ -144,6 +146,7 @@ export const getGlobalStoreModel = (): GlobalStoreModel => ({ requestedPriceEstimates: getRequestedPriceEstimatesModel(), recentPriceRanges: getRecentPriceRangesModel(), progressiveOnboarding: getProgressiveOnboardingModel(), + infiniteDiscovery: getInfiniteDiscoveryModel(), setSessionState: action((state, payload) => { state.sessionState = { ...state.sessionState, ...payload } diff --git a/src/app/store/InfiniteDiscoveryModel.ts b/src/app/store/InfiniteDiscoveryModel.ts new file mode 100644 index 00000000000..8fb85a4d836 --- /dev/null +++ b/src/app/store/InfiniteDiscoveryModel.ts @@ -0,0 +1,15 @@ +import { Action, action } from "easy-peasy" + +export interface InfiniteDiscoveryModel { + discoveredArtworkIds: string[] + addDiscoveredArtworkId: Action +} + +export const getInfiniteDiscoveryModel = (): InfiniteDiscoveryModel => ({ + discoveredArtworkIds: [], + addDiscoveredArtworkId: action((state, artworkId) => { + if (!state.discoveredArtworkIds.includes(artworkId)) { + state.discoveredArtworkIds.push(artworkId) + } + }), +}) diff --git a/src/app/store/__tests__/migration.tests.ts b/src/app/store/__tests__/migration.tests.ts index 5c989f425ff..2675fb934d7 100644 --- a/src/app/store/__tests__/migration.tests.ts +++ b/src/app/store/__tests__/migration.tests.ts @@ -1041,3 +1041,23 @@ describe("App version Versions.AddProgressiveOnboardingModel", () => { }) }) }) + +describe("App version Versions.AddInfiniteDiscoveryModel", () => { + it("adds infiniteScrolling to the UserPrefs model", () => { + const migrationToTest = Versions.AddInfiniteDiscoveryModel + + const previousState = migrate({ + state: { version: 0 }, + toVersion: migrationToTest - 1, + }) as any + + const migratedState = migrate({ + state: previousState, + toVersion: migrationToTest, + }) as any + + expect(migratedState.infiniteDiscovery).toEqual({ + discoveredArtworkIds: [], + }) + }) +}) diff --git a/src/app/store/migration.ts b/src/app/store/migration.ts index 413cc10877d..457d631a825 100644 --- a/src/app/store/migration.ts +++ b/src/app/store/migration.ts @@ -57,9 +57,10 @@ export const Versions = { DeleteDirtyArtworkDetails: 44, AddSubmissionDraft: 45, DeleteArtworkAndArtistViewOption: 46, + AddInfiniteDiscoveryModel: 47, } -export const CURRENT_APP_VERSION = Versions.DeleteArtworkAndArtistViewOption +export const CURRENT_APP_VERSION = Versions.AddInfiniteDiscoveryModel export type Migrations = Record any> export const artsyAppMigrations: Migrations = { @@ -330,6 +331,11 @@ export const artsyAppMigrations: Migrations = { delete state.userPrefs.artworkViewOption delete state.userPrefs.artistViewOption }, + [Versions.AddInfiniteDiscoveryModel]: (state) => { + state.infiniteDiscovery = { + discoveredArtworkIds: [], + } + }, } export function migrate({