Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(DIA-1062): persist discovered artwork IDs sent to infinite discovery endpoint #11421

Merged
merged 6 commits into from
Jan 23, 2025
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
122 changes: 74 additions & 48 deletions src/app/Scenes/InfiniteDiscovery/InfiniteDiscovery.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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<NonNullable<InfiniteDiscoveryQuery$data["discoverArtworks"]>["edges"]>[0]
>["node"]
>

export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({ 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<Artwork[]>([])

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<InfiniteDiscoveryQuery>(
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 <InfiniteDiscoverySpinner />
}

const goToPrevious = () => {
if (index > 0) {
Expand All @@ -64,17 +81,35 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({ 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<InfiniteDiscoveryQuery>(
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)
},
})
}
}

Expand Down Expand Up @@ -173,24 +208,21 @@ export const InfiniteDiscovery: React.FC<InfiniteDiscoveryProps> = ({ artworks:
)
}

export const InfiniteDiscoveryQueryRenderer = withSuspense({
Component: () => {
const initialData = useLazyLoadQuery<InfiniteDiscoveryQuery>(infiniteDiscoveryQuery, {})

if (!initialData) {
return null
}
const InfiniteDiscoverySpinner: React.FC = () => (
<Screen>
<Screen.Body fullwidth>
<Screen.Header title="Discovery" />
<Flex flex={1} justifyContent="center" alignItems="center">
<Spinner />
</Flex>
</Screen.Body>
</Screen>
)

return <InfiniteDiscovery artworks={initialData} />
},
LoadingFallback: SpinnerFallback,
ErrorFallback: NoFallback,
})
export const InfiniteDiscoveryQueryRenderer = () => <InfiniteDiscovery />

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 {
Expand Down Expand Up @@ -218,9 +250,3 @@ const infiniteDiscoveryFragment = graphql`
}
}
`

export const infiniteDiscoveryQuery = graphql`
query InfiniteDiscoveryQuery {
...InfiniteDiscovery_Fragment
}
`
Original file line number Diff line number Diff line change
@@ -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<typeof fetchQuery>

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<typeof fetchQuery>) => {
mockFetchQuery.mockReturnValueOnce(
Observable.from(
Promise.resolve({
discoverArtworks: {
edges: [
{
node: {
internalID: "artwork-1",
},
},
{
node: {
internalID: "artwork-2",
},
},
],
},
},
],
}),
})
)
)
renderWithWrappers(<InfiniteDiscovery />)
await screen.findByTestId("top-fancy-swiper-card")
}
3 changes: 3 additions & 0 deletions src/app/store/GlobalStoreModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -57,6 +58,7 @@ interface GlobalStoreStateModel {
requestedPriceEstimates: RequestedPriceEstimatesModel
recentPriceRanges: RecentPriceRangesModel
progressiveOnboarding: ProgressiveOnboardingModel
infiniteDiscovery: InfiniteDiscoveryModel
}
export interface GlobalStoreModel extends GlobalStoreStateModel {
rehydrate: Action<this, DeepPartial<State<GlobalStoreStateModel>>>
Expand Down Expand Up @@ -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 }
Expand Down
15 changes: 15 additions & 0 deletions src/app/store/InfiniteDiscoveryModel.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Action, action } from "easy-peasy"

export interface InfiniteDiscoveryModel {
discoveredArtworkIds: string[]
addDiscoveredArtworkId: Action<this, string>
}

export const getInfiniteDiscoveryModel = (): InfiniteDiscoveryModel => ({
discoveredArtworkIds: [],
addDiscoveredArtworkId: action((state, artworkId) => {
if (!state.discoveredArtworkIds.includes(artworkId)) {
state.discoveredArtworkIds.push(artworkId)
}
}),
})
damassi marked this conversation as resolved.
Show resolved Hide resolved
20 changes: 20 additions & 0 deletions src/app/store/__tests__/migration.tests.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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: [],
})
})
})
Loading