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: Prefetch all links as they enter the viewport #11293

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
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
21 changes: 10 additions & 11 deletions src/app/Components/ArtworkGrids/ArtworkGridItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,12 +26,12 @@ import { ContextMenuArtwork } from "app/Components/ContextMenu/ContextMenuArtwor
import { DurationProvider } from "app/Components/Countdown"
import { Disappearable, DissapearableArtwork } from "app/Components/Disappearable"
import { ProgressiveOnboardingSaveArtwork } from "app/Components/ProgressiveOnboarding/ProgressiveOnboardingSaveArtwork"
import { RouterLink } from "app/Components/RouterLink"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we place all system-level things in the System folder? https://github.com/artsy/eigen/tree/main/src/app/system/navigation

(in general, we should leverage this folder more for framework-y things -- cc @MounirDhahri)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea! I'll move the component to the systems folder 👍

import { HEART_ICON_SIZE } from "app/Components/constants"
import { PartnerOffer } from "app/Scenes/Activity/components/PartnerOfferCreatedNotification"
import { ArtworkItemCTAs } from "app/Scenes/Artwork/Components/ArtworkItemCTAs"
import { useGetNewSaveAndFollowOnArtworkCardExperimentVariant } from "app/Scenes/Artwork/utils/useGetNewSaveAndFollowOnArtworkCardExperimentVariant"
import { GlobalStore } from "app/store/GlobalStore"
import { navigate } from "app/system/navigation/navigate"
import { ElementInView } from "app/utils/ElementInView"
import { useArtworkBidding } from "app/utils/Websockets/auctions/useArtworkBidding"
import { getArtworkSignalTrackingFields } from "app/utils/getArtworkSignalTrackingFields"
Expand Down Expand Up @@ -226,16 +226,13 @@ export const Artwork: React.FC<ArtworkProps> = ({

addArtworkToRecentSearches()
trackArtworkTap()

if (artwork.href) {
if (partnerOffer && !!hasEnded && !!enablePartnerOfferOnArtworkScreen) {
navigate?.(artwork.href, { passProps: { artworkOfferExpired: true } })
} else {
navigate?.(artwork.href)
}
}
}

const passProps =
partnerOffer && !!hasEnded && !!enablePartnerOfferOnArtworkScreen
? { artworkOfferExpired: true }
: undefined

const trackArtworkTap = () => {
// Unless you explicitly pass in a tracking function or provide a contextScreenOwnerType, we won't track
// taps from the grid.
Expand Down Expand Up @@ -308,11 +305,13 @@ export const Artwork: React.FC<ArtworkProps> = ({
artwork={artwork}
hideCreateAlertOnArtworkPreview={hideCreateAlertOnArtworkPreview}
>
<Touchable
<RouterLink
haptic
underlayColor={color("white100")}
activeOpacity={0.8}
onPress={handleTap}
passProps={passProps}
to={artwork.href}
testID={`artworkGridItem-${artwork.title}`}
>
<View ref={itemRef}>
Expand Down Expand Up @@ -472,7 +471,7 @@ export const Artwork: React.FC<ArtworkProps> = ({
/>
</Flex>
</View>
</Touchable>
</RouterLink>
</ContextMenuArtwork>

<CreateArtworkAlertModal
Expand Down
63 changes: 63 additions & 0 deletions src/app/Components/RouterLink.tests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import { Text } from "@artsy/palette-mobile"
import { fireEvent, screen } from "@testing-library/react-native"
import { RouterLink } from "app/Components/RouterLink"
import { navigate } from "app/system/navigation/navigate"
import { renderWithWrappers } from "app/utils/tests/renderWithWrappers"
import { useEffect } from "react"
import { View } from "react-native"

const mockPrefetch = jest.fn()
const MockedVisibleSentinel: React.FC<any> = ({ children, onVisible }) => {
useEffect(() => onVisible(), [])

return <View>{children}</View>
}

jest.mock("app/utils/queryPrefetching", () => ({
usePrefetch: () => mockPrefetch,
}))
jest.mock("app/utils/ElementInView", () => ({
ElementInView: (props: any) => <MockedVisibleSentinel {...props} />,
}))

describe("RouterLink", () => {
afterEach(() => {
jest.clearAllMocks()
})

const TestComponent = (props: any) => (
<RouterLink to="/test-route" {...props}>
<Text>Test Link</Text>
</RouterLink>
)

it("renders", () => {
renderWithWrappers(<TestComponent />)

expect(screen.getByText("Test Link")).toBeDefined()
})

it("navigates to route on press", () => {
renderWithWrappers(<TestComponent />)

fireEvent.press(screen.getByText("Test Link"))

expect(navigate).toHaveBeenCalledWith("/test-route", { passProps: undefined })
})

describe("prefetching", () => {
it("prefetches ", () => {
renderWithWrappers(<TestComponent />)

expect(mockPrefetch).toHaveBeenCalledWith("/test-route")
})

describe("when disablePrefetch is true", () => {
renderWithWrappers(<TestComponent disablePrefetch />)

it("does not prefetch", () => {
expect(mockPrefetch).not.toHaveBeenCalledWith("/test-route")
})
})
})
})
43 changes: 43 additions & 0 deletions src/app/Components/RouterLink.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { Touchable, TouchableProps } from "@artsy/palette-mobile"
import { navigate } from "app/system/navigation/navigate"
import { ElementInView } from "app/utils/ElementInView"
import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag"
import { usePrefetch } from "app/utils/queryPrefetching"
import { GestureResponderEvent } from "react-native"

interface RouterLinkProps {
disablePrefetch?: boolean
passProps?: Object
to: string | null | undefined
}

export const RouterLink: React.FC<RouterLinkProps & TouchableProps> = ({
disablePrefetch,
to,
onPress,
passProps,
...restProps
}) => {
const prefetchUrl = usePrefetch()
const enableViewPortPrefetching = useFeatureFlag("AREnableViewPortPrefetching")

const handlePress = (event: GestureResponderEvent) => {
onPress?.(event)

if (to) {
navigate(to, { passProps })
}
}

const handleVisible = () => {
if (!disablePrefetch && enableViewPortPrefetching && to) {
prefetchUrl(to)
}
}

return (
<ElementInView onVisible={handleVisible}>
<Touchable {...restProps} onPress={handlePress} />
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder here if Touchable would be our preferred way, sometimes we use TouchableOpacity and sometimes we use TouchableWithoutHighlight.
Anyway, that's a problem for later, we can start with this now and extend it later.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe this is a call to use RouterTouchableOpacity 🤷

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I used the Touchable component from "@artsy/palette-mobile" and figured this is probably the preferred one. It has a prop to adjust the opacity on press.

Yeah, RouterTouchableOpacity or RouterTouchable could also be good names for this component. I took the name RouterLink from Force but haven't thought much about naming yet. I'm very open for ideas for the naming.

</ElementInView>
)
}
18 changes: 13 additions & 5 deletions src/app/Components/SectionTitle.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,31 @@
import { ArrowRightIcon, Flex, SpacingUnit, Text, TextProps, useTheme } from "@artsy/palette-mobile"
import { toTitleCase } from "@artsy/to-title-case"
import { TouchableOpacity } from "react-native"
import { RouterLink } from "app/Components/RouterLink"

const Wrapper: React.FC<{ onPress?(): any }> = ({ onPress, children }) => {
const Wrapper: React.FC<{ onPress?(): void; href?: string | null }> = ({
children,
href,
onPress,
}) => {
if (onPress) {
return (
<TouchableOpacity
<RouterLink
onPress={onPress}
to={href}
testID="touchable-wrapper"
hitSlop={{ top: 10, bottom: 10 }}
activeOpacity={0.65}
>
{children}
</TouchableOpacity>
</RouterLink>
)
} else {
return <>{children}</>
}
}

export const SectionTitle: React.FC<{
href?: string | null
title: React.ReactNode
titleVariant?: TextProps["variant"]
subtitle?: React.ReactNode
Expand All @@ -27,6 +34,7 @@ export const SectionTitle: React.FC<{
mb?: SpacingUnit
capitalized?: boolean
}> = ({
href,
title,
titleVariant = "sm-display",
subtitle,
Expand All @@ -43,7 +51,7 @@ export const SectionTitle: React.FC<{
}

return (
<Wrapper onPress={onPress}>
<Wrapper onPress={onPress} href={href}>
<Flex mb={mb} flexDirection="row" alignItems="flex-start">
<Flex flex={1}>
<Text variant={titleVariant} ellipsizeMode="tail" numberOfLines={1} testID="title">
Expand Down
9 changes: 8 additions & 1 deletion src/app/Navigation/routes.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,13 @@ import {
import { CityGuideView } from "app/NativeModules/CityGuideView"
import { LiveAuctionView } from "app/NativeModules/LiveAuctionView"
import { About } from "app/Scenes/About/About"
import { ActivityItemScreenQueryRenderer } from "app/Scenes/Activity/ActivityItemScreen"
import { activityContentQuery } from "app/Scenes/Activity/ActivityContent"
import {
ActivityItemQuery,
ActivityItemScreenQueryRenderer,
} from "app/Scenes/Activity/ActivityItemScreen"
import { ActivityScreen } from "app/Scenes/Activity/ActivityScreen"
import { activityHeaderQuery } from "app/Scenes/Activity/components/ActivityHeader"
import { ArtQuiz } from "app/Scenes/ArtQuiz/ArtQuiz"
import { ArtQuizResults } from "app/Scenes/ArtQuiz/ArtQuizResults/ArtQuizResults"
import { ArticleScreen } from "app/Scenes/Article/ArticleScreen"
Expand Down Expand Up @@ -222,6 +227,7 @@ export const artsyDotNetRoutes = defineRoutes([
headerShown: false,
},
},
Queries: [activityHeaderQuery, activityContentQuery],
},
{
path: "/notification/:notificationID",
Expand All @@ -233,6 +239,7 @@ export const artsyDotNetRoutes = defineRoutes([
headerShown: false,
},
},
Queries: [ActivityItemQuery],
},
{
path: "/art-quiz",
Expand Down
9 changes: 6 additions & 3 deletions src/app/Scenes/Activity/ActivityContent.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ export const ActivityContent: React.FC<ActivityProps> = ({ type }) => {
const queryData = useLazyLoadQuery<ActivityContentQuery>(
activityContentQuery,
{
count: 10,
types,
},
{
Expand All @@ -31,8 +30,12 @@ export const ActivityContent: React.FC<ActivityProps> = ({ type }) => {
return <ActivityList viewer={queryData.viewer} type={type} />
}

const activityContentQuery = graphql`
query ActivityContentQuery($count: Int, $after: String, $types: [NotificationTypesEnum]) {
export const activityContentQuery = graphql`
query ActivityContentQuery(
$count: Int = 10
$after: String
$types: [NotificationTypesEnum] = []
) {
viewer {
...ActivityList_viewer @arguments(count: $count, after: $after, types: $types)
}
Expand Down
19 changes: 11 additions & 8 deletions src/app/Scenes/Activity/ActivityItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@ import { ActionType } from "@artsy/cohesion"
import { ClickedActivityPanelNotificationItem } from "@artsy/cohesion/dist/Schema/Events/ActivityPanel"
import { Flex, Image, Text } from "@artsy/palette-mobile"
import { ActivityItem_notification$key } from "__generated__/ActivityItem_notification.graphql"
import { RouterLink } from "app/Components/RouterLink"
import { CollectorUpdateNotification } from "app/Scenes/Activity/components/CollectorUpdateNotification"
import {
ExpiresInTimer,
shouldDisplayExpiresInTimer,
} from "app/Scenes/Activity/components/ExpiresInTimer"
import { PartnerOfferBadge } from "app/Scenes/Activity/components/PartnerOffeBadge"
import { useMarkNotificationAsRead } from "app/Scenes/Activity/mutations/useMarkNotificationAsRead"
import { navigateToActivityItem } from "app/Scenes/Activity/utils/navigateToActivityItem"
import { getActivityItemHref } from "app/Scenes/Activity/utils/getActivityItemHref"
import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag"
import { memo } from "react"
import { TouchableOpacity } from "react-native"
import { graphql, useFragment } from "react-relay"
import { useTracking } from "react-tracking"
import { ActivityItemTypeLabel } from "./ActivityItemTypeLabel"
Expand Down Expand Up @@ -48,16 +48,19 @@ export const ActivityItem: React.FC<ActivityItemProps> = memo(
if (notification.isUnread) {
markAsRead(notification)
}

if (!isCollectorProfileUpdate) {
navigateToActivityItem(notification)
}
}

const showAsRow = isPartnerOffer

const { href, passProps } = getActivityItemHref(notification)

return (
<TouchableOpacity activeOpacity={0.65} onPress={handlePress}>
<RouterLink
activeOpacity={0.65}
onPress={handlePress}
to={isCollectorProfileUpdate ? undefined : href}
passProps={passProps}
>
<Flex flexDirection="row" alignItems="center" justifyContent="space-between" px={2}>
{!!isCollectorProfileUpdate ? (
<CollectorUpdateNotification
Expand Down Expand Up @@ -146,7 +149,7 @@ export const ActivityItem: React.FC<ActivityItemProps> = memo(
/>
)}
</Flex>
</TouchableOpacity>
</RouterLink>
)
},
(prevProps, nextProps) => {
Expand Down
8 changes: 4 additions & 4 deletions src/app/Scenes/Activity/ActivityItemScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ export const ActivityItemScreenQueryRenderer: FC<ActivityItemScreenQueryRenderer
withSuspense({
Component: ({ notificationID }) => {
const data = useLazyLoadQuery<ActivityItemScreenQuery>(ActivityItemQuery, {
internalID: notificationID,
notificationID,
})

const notification = data.me?.notification
Expand Down Expand Up @@ -83,10 +83,10 @@ export const ActivityItemScreenQueryRenderer: FC<ActivityItemScreenQueryRenderer
},
})

const ActivityItemQuery = graphql`
query ActivityItemScreenQuery($internalID: String!) {
export const ActivityItemQuery = graphql`
query ActivityItemScreenQuery($notificationID: String!) {
me {
notification(id: $internalID) {
notification(id: $notificationID) {
item {
__typename
}
Expand Down
4 changes: 2 additions & 2 deletions src/app/Scenes/Activity/components/ActivityHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { ActivityScreenStore } from "app/Scenes/Activity/ActivityScreenStore"
import { graphql, useLazyLoadQuery } from "react-relay"

export const ActivityHeader: React.FC = () => {
const data = useLazyLoadQuery<ActivityHeaderQuery>(query, {})
const data = useLazyLoadQuery<ActivityHeaderQuery>(activityHeaderQuery, {})

const type = ActivityScreenStore.useStoreState((state) => state.type)
const setType = ActivityScreenStore.useStoreActions((actions) => actions.setType)
Expand Down Expand Up @@ -48,7 +48,7 @@ export const ActivityHeader: React.FC = () => {
)
}

const query = graphql`
export const activityHeaderQuery = graphql`
query ActivityHeaderQuery {
viewer {
partnerOfferNotifications: notificationsConnection(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,17 @@ import { ActivityRailItem_item$data } from "__generated__/ActivityRailItem_item.
import { FilterArray } from "app/Components/ArtworkFilter/ArtworkFilterHelpers"
import { ORDERED_ARTWORK_SORTS } from "app/Components/ArtworkFilter/Filters/SortOptions"
import { SUPPORTED_NOTIFICATION_TYPES } from "app/Scenes/Activity/ActivityItemScreen"
import { navigate } from "app/system/navigation/navigate"
import { matchRoute } from "app/system/navigation/utils/matchRoute"
import { last } from "lodash"
import { parse as parseQueryString } from "query-string"

export const navigateToActivityItem = (
export const getActivityItemHref = (
item: ActivityItem_notification$data | ActivityRailItem_item$data
) => {
const { internalID, targetHref, notificationType } = item

if (SUPPORTED_NOTIFICATION_TYPES.includes(notificationType)) {
return navigate(`/notification/${internalID}`)
return { href: `/notification/${internalID}` }
}

const splittedQueryParams = targetHref.split("?")
Expand All @@ -34,7 +33,5 @@ export const navigateToActivityItem = (
passProps.scrollToArtworksGrid = true
}

navigate(targetHref, {
passProps,
})
return { href: targetHref, passProps }
}
Loading