From 43a4c78c54dfda13618b043a6cad5e452913246c Mon Sep 17 00:00:00 2001 From: Mounir Dhahri Date: Wed, 13 Nov 2024 11:06:33 +0100 Subject: [PATCH] feat: wip --- .../ArtworkGrids/ArtworkGridItem.tsx | 85 +++--------- .../ArtworkGridItemSaveButton.tsx | 124 ++++++++++++++++++ src/app/Scenes/HomeView/Sections/Section.tsx | 86 ++++++------ src/app/system/ignoreLogs.ts | 1 + src/app/system/relay/defaultEnvironment.ts | 14 +- .../helpers/cacheHeaderMiddlewareHelpers.ts | 17 +++ .../middlewares/cacheHeaderMiddleware.ts | 12 ++ .../relay/middlewares/timingMiddleware.ts | 3 + .../utils/fetchArtworkNonCacheableFields.ts | 31 +++++ 9 files changed, 255 insertions(+), 118 deletions(-) create mode 100644 src/app/Components/ArtworkGrids/ArtworkGridItemSaveButton.tsx create mode 100644 src/app/utils/fetchArtworkNonCacheableFields.ts diff --git a/src/app/Components/ArtworkGrids/ArtworkGridItem.tsx b/src/app/Components/ArtworkGrids/ArtworkGridItem.tsx index 2d043c94ef2..99868036059 100644 --- a/src/app/Components/ArtworkGrids/ArtworkGridItem.tsx +++ b/src/app/Components/ArtworkGrids/ArtworkGridItem.tsx @@ -2,8 +2,6 @@ import { ActionType, ContextModule, OwnerType, TappedMainArtworkGrid } from "@ar import { Box, Flex, - HeartFillIcon, - HeartIcon, Image, Skeleton, SkeletonBox, @@ -19,14 +17,12 @@ import { CreateArtworkAlertModal } from "app/Components/Artist/ArtistArtworks/Cr import { filterArtworksParams } from "app/Components/ArtworkFilter/ArtworkFilterHelpers" import { ArtworksFiltersStore } from "app/Components/ArtworkFilter/ArtworkFilterStore" import { ArtworkAuctionTimer } from "app/Components/ArtworkGrids/ArtworkAuctionTimer" +import { ArtworkGridItemSaveButtonQueryRenderer } from "app/Components/ArtworkGrids/ArtworkGridItemSaveButton" import { ArtworkSocialSignal } from "app/Components/ArtworkGrids/ArtworkSocialSignal" -import { useSaveArtworkToArtworkLists } from "app/Components/ArtworkLists/useSaveArtworkToArtworkLists" import { ArtworkSaleMessage } from "app/Components/ArtworkRail/ArtworkSaleMessage" import { ContextMenuArtwork } from "app/Components/ContextMenu/ContextMenuArtwork" import { DurationProvider } from "app/Components/Countdown" import { Disappearable, DissapearableArtwork } from "app/Components/Disappearable" -import { ProgressiveOnboardingSaveArtwork } from "app/Components/ProgressiveOnboarding/ProgressiveOnboardingSaveArtwork" -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 { getNewSaveAndFollowOnArtworkCardExperimentVariant } from "app/Scenes/Artwork/utils/getNewSaveAndFollowOnArtworkCardExperimentVariant" @@ -34,16 +30,13 @@ import { GlobalStore } from "app/store/GlobalStore" import { navigate } from "app/system/navigation/navigate" import { useArtworkBidding } from "app/utils/Websockets/auctions/useArtworkBidding" import { useExperimentVariant } from "app/utils/experiments/hooks" +import { useArtworkNonCacheableFields } from "app/utils/fetchArtworkNonCacheableFields" import { getArtworkSignalTrackingFields } from "app/utils/getArtworkSignalTrackingFields" import { saleMessageOrBidInfo } from "app/utils/getSaleMessgeOrBidInfo" import { getTimer } from "app/utils/getTimer" import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" -import { useSaveArtwork } from "app/utils/mutations/useSaveArtwork" import { RandomNumberGenerator } from "app/utils/placeholders" -import { - ArtworkActionTrackingProps, - tracks as artworkActionTracks, -} from "app/utils/track/ArtworkActions" +import { ArtworkActionTrackingProps } from "app/utils/track/ArtworkActions" import React, { useRef, useState } from "react" import { View, ViewProps } from "react-native" import { createFragmentContainer, graphql } from "react-relay" @@ -129,6 +122,10 @@ export const Artwork: React.FC = ({ "onyx_artwork-card-save-and-follow-cta-redesign" ) + const nonCacheableData = useArtworkNonCacheableFields(artwork.internalID) + + console.log({ isSavedNonCacheable: nonCacheableData?.artwork?.isSaved }) + const { enableShowOldSaveCTA, enableNewSaveCTA, enableNewSaveAndFollowCTAs } = getNewSaveAndFollowOnArtworkCardExperimentVariant( newSaveAndFollowOnArtworkCardExperiment.enabled, @@ -179,35 +176,6 @@ export const Artwork: React.FC = ({ } } - const onArtworkSavedOrUnSaved = (saved: boolean) => { - const { availability, isAcquireable, isBiddable, isInquireable, isOfferable } = artwork - const params = { - acquireable: isAcquireable, - availability, - biddable: isBiddable, - context_module: contextModule, - context_screen: contextScreen, - context_screen_owner_id: contextScreenOwnerId, - context_screen_owner_slug: contextScreenOwnerSlug, - context_screen_owner_type: contextScreenOwnerType, - inquireable: isInquireable, - offerable: isOfferable, - } - tracking.trackEvent(artworkActionTracks.saveOrUnsaveArtwork(saved, params)) - } - - const { isSaved, saveArtworkToLists } = useSaveArtworkToArtworkLists({ - artworkFragmentRef: artwork, - onCompleted: onArtworkSavedOrUnSaved, - }) - - const handleArtworkSave = useSaveArtwork({ - id: artwork.id, - internalID: artwork.internalID, - isSaved: !!artwork.isSaved, - onCompleted: onArtworkSavedOrUnSaved, - }) - const { hasEnded } = getTimer(partnerOffer?.endAt || "") const handleTap = () => { @@ -425,13 +393,16 @@ export const Artwork: React.FC = ({ {collectorSignals.auction.lotWatcherCount} )} - - - + )} @@ -463,26 +434,6 @@ export const Artwork: React.FC = ({ ) } -const ArtworkHeartIcon: React.FC<{ isSaved: boolean | null; index?: number }> = ({ - isSaved, - index, -}) => { - const iconProps = { height: HEART_ICON_SIZE, width: HEART_ICON_SIZE, testID: "empty-heart-icon" } - - if (isSaved) { - return - } - if (index === 0) { - // We only try to show the save onboard Popover in the 1st element - return ( - - - - ) - } - return -} - export default createFragmentContainer(Artwork, { artwork: graphql` fragment ArtworkGridItem_artwork on Artwork @@ -510,7 +461,6 @@ export default createFragmentContainer(Artwork, { isBiddable isInquireable isOfferable - isSaved isUnlisted artistNames href @@ -571,7 +521,6 @@ export default createFragmentContainer(Artwork, { ...ArtworkSocialSignal_collectorSignals } ...ArtworkSaleMessage_artwork - ...useSaveArtworkToArtworkLists_artwork } `, }) diff --git a/src/app/Components/ArtworkGrids/ArtworkGridItemSaveButton.tsx b/src/app/Components/ArtworkGrids/ArtworkGridItemSaveButton.tsx new file mode 100644 index 00000000000..8f6040ef257 --- /dev/null +++ b/src/app/Components/ArtworkGrids/ArtworkGridItemSaveButton.tsx @@ -0,0 +1,124 @@ +import { Flex, HeartFillIcon, HeartIcon, Text, Touchable } from "@artsy/palette-mobile" +import { + ArtworkGridItemSaveButtonQuery, + ArtworkGridItemSaveButtonQuery$data, +} from "__generated__/ArtworkGridItemSaveButtonQuery.graphql" +import { useSaveArtworkToArtworkLists } from "app/Components/ArtworkLists/useSaveArtworkToArtworkLists" +import { ProgressiveOnboardingSaveArtwork } from "app/Components/ProgressiveOnboarding/ProgressiveOnboardingSaveArtwork" +import { HEART_ICON_SIZE } from "app/Components/constants" +import { NoFallback } from "app/utils/hooks/withSuspense" +import { useSaveArtwork } from "app/utils/mutations/useSaveArtwork" +import { ArtworkActionTrackingProps, tracks } from "app/utils/track/ArtworkActions" +import { Suspense } from "react" +import { graphql, useLazyLoadQuery } from "react-relay" +import { useTracking } from "react-tracking" + +interface ArtworkGridItemSaveButtonProps extends ArtworkActionTrackingProps { + disableArtworksListPrompt: boolean + id: string + itemIndex?: number +} + +export const ArtworkGridItemSaveButton = ({ + contextModule, + contextScreen, + contextScreenOwnerId, + contextScreenOwnerSlug, + contextScreenOwnerType, + disableArtworksListPrompt, + id, + itemIndex, +}: ArtworkGridItemSaveButtonProps) => { + const { trackEvent } = useTracking() + const data = useLazyLoadQuery( + graphql` + query ArtworkGridItemSaveButtonQuery($id: String!) { + artwork(id: $id) { + id + internalID + isSaved + availability + isAcquireable + isBiddable + isInquireable + isOfferable + ...useSaveArtworkToArtworkLists_artwork + } + } + `, + { + id, + } + ) + + const artwork = data.artwork as NonNullable + + const onArtworkSavedOrUnSaved = (saved: boolean) => { + const params = { + acquireable: artwork.isAcquireable, + availability: artwork.availability, + biddable: artwork.isBiddable, + context_module: contextModule, + context_screen_owner_id: contextScreenOwnerId, + context_screen_owner_slug: contextScreenOwnerSlug, + context_screen_owner_type: contextScreenOwnerType, + context_screen: contextScreen, + inquireable: artwork.isInquireable, + offerable: artwork.isOfferable, + } + trackEvent(tracks.saveOrUnsaveArtwork(saved, params)) + } + + const handleArtworkSave = useSaveArtwork({ + id: artwork.id, + internalID: id, + isSaved: !!data?.artwork?.isSaved, + onCompleted: onArtworkSavedOrUnSaved, + }) + + const { isSaved, saveArtworkToLists } = useSaveArtworkToArtworkLists({ + artworkFragmentRef: artwork, + onCompleted: onArtworkSavedOrUnSaved, + }) + if (!data.artwork) { + return null + } + + return ( + + + + ) +} + +const ArtworkHeartIcon: React.FC<{ isSaved: boolean | null; index?: number }> = ({ + isSaved, + index, +}) => { + const iconProps = { height: HEART_ICON_SIZE, width: HEART_ICON_SIZE, testID: "empty-heart-icon" } + + if (isSaved) { + return + } + if (index === 0) { + // We only try to show the save onboard Popover in the 1st element + return ( + + + + ) + } + return +} + +export const ArtworkGridItemSaveButtonQueryRenderer = (props: ArtworkGridItemSaveButtonProps) => { + return ( + + + + ) +} diff --git a/src/app/Scenes/HomeView/Sections/Section.tsx b/src/app/Scenes/HomeView/Sections/Section.tsx index 78cc2b0eaa9..65664e92503 100644 --- a/src/app/Scenes/HomeView/Sections/Section.tsx +++ b/src/app/Scenes/HomeView/Sections/Section.tsx @@ -42,54 +42,54 @@ export const Section: React.FC = ({ section, ...rest }) => { } switch (section.component?.type) { - case "FeaturedCollection": - return ( - - ) - case "ArticlesCard": - return + // case "FeaturedCollection": + // return ( + // + // ) + // case "ArticlesCard": + // return case "Chips": return } switch (section.__typename) { - case "HomeViewSectionActivity": - return - case "HomeViewSectionArtworks": - return - case "HomeViewSectionCard": - return - case "HomeViewSectionGeneric": - return - case "HomeViewSectionArticles": - return - case "HomeViewSectionArtists": - return - case "HomeViewSectionAuctionResults": - return - case "HomeViewSectionHeroUnits": - return - case "HomeViewSectionCards": - return - case "HomeViewSectionFairs": - return - case "HomeViewSectionMarketingCollections": - return ( - - ) - case "HomeViewSectionShows": - return - case "HomeViewSectionViewingRooms": - return - case "HomeViewSectionSales": - return - case "HomeViewSectionTasks": - return enableHomeViewTasksSection ? ( - - ) : null + // case "HomeViewSectionActivity": + // return + // case "HomeViewSectionArtworks": + // return + // case "HomeViewSectionCard": + // return + // case "HomeViewSectionGeneric": + // return + // case "HomeViewSectionArticles": + // return + // case "HomeViewSectionArtists": + // return + // case "HomeViewSectionAuctionResults": + // return + // case "HomeViewSectionHeroUnits": + // return + // case "HomeViewSectionCards": + // return + // case "HomeViewSectionFairs": + // return + // case "HomeViewSectionMarketingCollections": + // return ( + // + // ) + // case "HomeViewSectionShows": + // return + // case "HomeViewSectionViewingRooms": + // return + // case "HomeViewSectionSales": + // return + // case "HomeViewSectionTasks": + // return enableHomeViewTasksSection ? ( + // + // ) : null default: if (__DEV__) { return ( diff --git a/src/app/system/ignoreLogs.ts b/src/app/system/ignoreLogs.ts index 5df920dafcd..ae3560abaa2 100644 --- a/src/app/system/ignoreLogs.ts +++ b/src/app/system/ignoreLogs.ts @@ -14,4 +14,5 @@ LogBox.ignoreLogs([ // This is for the Artist page, which will likely get redone soon anyway. "VirtualizedLists should never be nested inside plain ScrollViews with the same orientation - use another VirtualizedList-backed container instead.", "Picker has been extracted", + "[Reanimated]", ]) diff --git a/src/app/system/relay/defaultEnvironment.ts b/src/app/system/relay/defaultEnvironment.ts index c961a1a49bd..24695afb535 100644 --- a/src/app/system/relay/defaultEnvironment.ts +++ b/src/app/system/relay/defaultEnvironment.ts @@ -28,15 +28,15 @@ export let _globalCacheRef: RelayQueryResponseCache | undefined const network = new RelayNetworkLayer( [ // middlewares use LIFO. The bottom ones in the array will run first after the fetch. - cacheMiddleware({ - size: 500, // max 500 requests - ttl: 900000, // 1 hour - clearOnMutation: true, - onInit: (cache) => (_globalCacheRef = cache), - }), + // cacheMiddleware({ + // size: 500, // max 500 requests + // ttl: 900000, // 1 hour + // clearOnMutation: true, + // onInit: (cache) => (_globalCacheRef = cache), + // }), persistedQueryMiddleware(), metaphysicsURLMiddleware(), - rateLimitMiddleware(), + // rateLimitMiddleware(), uploadMiddleware(), // @ts-expect-error errorMiddleware(), diff --git a/src/app/system/relay/helpers/cacheHeaderMiddlewareHelpers.ts b/src/app/system/relay/helpers/cacheHeaderMiddlewareHelpers.ts index f78d6ea1498..74f5f97f3cc 100644 --- a/src/app/system/relay/helpers/cacheHeaderMiddlewareHelpers.ts +++ b/src/app/system/relay/helpers/cacheHeaderMiddlewareHelpers.ts @@ -1,5 +1,6 @@ import { GraphQLRequest } from "app/system/relay/middlewares/types" import { Variables } from "react-relay" +const queryMap = require("../../../../../data/complete.queryMap.json") export const CACHEABLE_DIRECTIVE_REGEX = /@\bcacheable\b/ export const CACHEABLE_ARGUMENT_REGEX = /"cacheable":true/ @@ -34,3 +35,19 @@ export const hasPersonalizedArguments = (variables: Variables) => { // return true if variables has at least one of the SKIP_CACHE_ARGUMENTS that is truthy return SKIP_CACHE_ARGUMENTS.some((arg) => !!variables[arg]) } + +export const SKIP_CACHE_FIELDS = ["isFollowed", "isSaved"] +// Important - Add any new personalized field checks to this list. That way, logged-in queries +// _without_ this field can still be `@cacheable`, and when queries include this field, +// those queries will not be cached. +export const hasPersonalizedFields = (request: GraphQLRequest) => { + const queryID = request.getID() + const body = queryMap[queryID] + + // Body doesn't include any of the strings in SKIP_CACHE_FIELDS + if ((body && body.includes("isFollowed")) || body.includes("isSaved")) { + return true + } + + return false +} diff --git a/src/app/system/relay/middlewares/cacheHeaderMiddleware.ts b/src/app/system/relay/middlewares/cacheHeaderMiddleware.ts index 878595663a8..6103b4a25d6 100644 --- a/src/app/system/relay/middlewares/cacheHeaderMiddleware.ts +++ b/src/app/system/relay/middlewares/cacheHeaderMiddleware.ts @@ -3,8 +3,10 @@ import { unsafe_getFeatureFlag } from "app/store/GlobalStore" import { hasNoCacheParamPresent, hasPersonalizedArguments, + hasPersonalizedFields, isRequestCacheable, SKIP_CACHE_ARGUMENTS, + SKIP_CACHE_FIELDS, } from "app/system/relay/helpers/cacheHeaderMiddlewareHelpers" import { GraphQLRequest } from "app/system/relay/middlewares/types" import { Middleware } from "react-relay-network-modern" @@ -50,6 +52,16 @@ export const shouldSkipCDNCache = (req: GraphQLRequest) => { return true } + if (typeof req.fetchOpts.body === "string" && hasPersonalizedFields(req)) { + if (__DEV__) { + console.warn( + `You are setting a personalized field on a @cacheable request, CDN cache will be ignored for ${req.operation.name}. \nList of personalized fields: `, + SKIP_CACHE_FIELDS.join(", ") + ) + } + return true + } + // Use CDN cache if none of the above conditions are met return false } diff --git a/src/app/system/relay/middlewares/timingMiddleware.ts b/src/app/system/relay/middlewares/timingMiddleware.ts index 77749ff2ab8..debdb725bbc 100644 --- a/src/app/system/relay/middlewares/timingMiddleware.ts +++ b/src/app/system/relay/middlewares/timingMiddleware.ts @@ -8,6 +8,9 @@ export function timingMiddleware() { // @ts-expect-error STRICTNESS_MIGRATION --- 🚨 Unsafe legacy code 🚨 Please delete this and fix any type errors if you have time 🙏 return next(req).then((res) => { const duration = Date.now() - startTime + if (operation.includes("Collection")) { + console.log({ duration, operation }) + } volleyClient.send({ type: "timing", name: "graphql-request-duration", diff --git a/src/app/utils/fetchArtworkNonCacheableFields.ts b/src/app/utils/fetchArtworkNonCacheableFields.ts new file mode 100644 index 00000000000..18aaa241288 --- /dev/null +++ b/src/app/utils/fetchArtworkNonCacheableFields.ts @@ -0,0 +1,31 @@ +import { fetchArtworkNonCacheableFieldsQuery } from "__generated__/fetchArtworkNonCacheableFieldsQuery.graphql" +import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" +import { useEffect, useState } from "react" +import { fetchQuery, graphql } from "react-relay" + +export const fetchArtworkNonCacheableFields = async (artworkID: string) => { + return fetchQuery( + getRelayEnvironment(), + graphql` + query fetchArtworkNonCacheableFieldsQuery($id: String!) { + artwork(id: $id) { + isSaved + } + } + `, + { id: artworkID } + ).toPromise() +} + +export const useArtworkNonCacheableFields = (artworkID: string) => { + const [data, setData] = useState() + useEffect(() => { + fetchArtworkNonCacheableFields(artworkID).then((res) => { + if (res) { + setData(res) + } + }) + }, [artworkID]) + + return data +}