diff --git a/.secrets.baseline b/.secrets.baseline index 78318565180..0c801e9ac18 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1134,5 +1134,5 @@ } ] }, - "generated_at": "2024-09-11T13:12:04Z" + "generated_at": "2024-10-24T13:23:36Z" } diff --git a/src/app/Components/ArtworkGrids/ArtworkGridItem.tests.tsx b/src/app/Components/ArtworkGrids/ArtworkGridItem.tests.tsx index 8bea4192f08..fa1dfb56e2f 100644 --- a/src/app/Components/ArtworkGrids/ArtworkGridItem.tests.tsx +++ b/src/app/Components/ArtworkGrids/ArtworkGridItem.tests.tsx @@ -491,14 +491,15 @@ describe("ArtworkGridItem", () => { renderWithRelay({ Artwork: () => ({ ...artwork, + saleMessage: "$120,500", sale: { ...artwork.sale, isAuction: false }, realizedPrice: null, collectorSignals, }), }) - expect(screen.getByText("Limited-Time Offer")).toBeOnTheScreen() - expect(screen.getByText("Exp. 1d 12h")).toBeOnTheScreen() + expect(screen.getByText("$120,500")).toBeOnTheScreen() + expect(screen.getByText("Offer Expires 1d 12h")).toBeOnTheScreen() }) it("doesn't show the limited-time offer signal for auction artworks", () => { diff --git a/src/app/Components/ArtworkGrids/ArtworkGridItem.tsx b/src/app/Components/ArtworkGrids/ArtworkGridItem.tsx index 4a75e208f30..1d150475745 100644 --- a/src/app/Components/ArtworkGrids/ArtworkGridItem.tsx +++ b/src/app/Components/ArtworkGrids/ArtworkGridItem.tsx @@ -21,13 +21,13 @@ import { ArtworksFiltersStore } from "app/Components/ArtworkFilter/ArtworkFilter import { ArtworkAuctionTimer } from "app/Components/ArtworkGrids/ArtworkAuctionTimer" 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/NotificationArtworkList" -import { formattedTimeLeft } from "app/Scenes/Activity/utils/formattedTimeLeft" import { GlobalStore } from "app/store/GlobalStore" import { navigate } from "app/system/navigation/navigate" import { useArtworkBidding } from "app/utils/Websockets/auctions/useArtworkBidding" @@ -252,10 +252,6 @@ export const Artwork: React.FC = ({ ? currentBiddingEndAt : artwork.saleArtwork?.endAt || artwork.sale?.endAt - const partnerOfferEndAt = collectorSignals?.partnerOffer?.endAt - ? formattedTimeLeft(getTimer(collectorSignals.partnerOffer.endAt).time).timerCopy - : "" - const urgencyTag = getUrgencyTag(endsAt) const canShowAuctionProgressBar = @@ -271,16 +267,13 @@ export const Artwork: React.FC = ({ const displayAuctionSignal = AREnableAuctionImprovementsSignals && isAuction - const saleInfoTextColor = - displayAuctionSignal && collectorSignals?.auction?.liveBiddingStarted ? "blue100" : "black100" - - const saleInfoTextWeight = - displayAuctionSignal && collectorSignals?.auction?.liveBiddingStarted ? "normal" : "bold" - const handleSupress = async (item: DissapearableArtwork) => { await item._disappearable?.disappear() } + const displayArtworkSocialSignal = + !isAuction && !displayLimitedTimeOfferSignal && !!collectorSignals + return ( ((artwork as any)._disappearable = ref)}> = ({ style={artworkMetaStyle} > - {!!displayLimitedTimeOfferSignal && ( - - - Limited-Time Offer - - - )} - {!isAuction && !displayLimitedTimeOfferSignal && !!collectorSignals && ( - - )} {!!showLotLabel && !!artwork.saleArtwork?.lotLabel && ( <> @@ -435,29 +414,12 @@ export const Artwork: React.FC = ({ )} {!!saleInfo && !hideSaleInfo && !displayPriceOfferMessage && ( - - {saleInfo} - {!!displayLimitedTimeOfferSignal && ( - - {" "} - Exp. {partnerOfferEndAt} - - )} - + )} {!!artwork.isUnlisted && ( @@ -472,6 +434,14 @@ export const Artwork: React.FC = ({ hideRegisterBySignal={hideRegisterBySignal} /> )} + + {!!displayArtworkSocialSignal && ( + + )} {!hideSaveIcon && ( @@ -609,6 +579,7 @@ export default createFragmentContainer(Artwork, { ...ArtworkAuctionTimer_collectorSignals ...ArtworkSocialSignal_collectorSignals } + ...ArtworkSaleMessage_artwork ...useSaveArtworkToArtworkLists_artwork } `, diff --git a/src/app/Components/ArtworkGrids/ArtworkSocialSignal.tsx b/src/app/Components/ArtworkGrids/ArtworkSocialSignal.tsx index e496dcd73af..ed1d07beeb8 100644 --- a/src/app/Components/ArtworkGrids/ArtworkSocialSignal.tsx +++ b/src/app/Components/ArtworkGrids/ArtworkSocialSignal.tsx @@ -1,4 +1,4 @@ -import { Box, Text } from "@artsy/palette-mobile" +import { ArrowUpRightIcon, Box, FireIcon, Text } from "@artsy/palette-mobile" import { ArtworkSocialSignal_collectorSignals$key } from "__generated__/ArtworkSocialSignal_collectorSignals.graphql" import { graphql, useFragment } from "react-relay" @@ -21,15 +21,10 @@ export const ArtworkSocialSignal: React.FC = ({ switch (true) { case curatorsPick && !hideCuratorsPick: return ( - - + + + + {" "} Curators’ Pick @@ -37,15 +32,10 @@ export const ArtworkSocialSignal: React.FC = ({ case increasedInterest && !hideIncreasedInterest: return ( - - + + + + {" "} Increased Interest diff --git a/src/app/Components/ArtworkRail/ArtworkRailCard.tests.tsx b/src/app/Components/ArtworkRail/ArtworkRailCard.tests.tsx index a4fb5ee81e5..0a4d2701437 100644 --- a/src/app/Components/ArtworkRail/ArtworkRailCard.tests.tsx +++ b/src/app/Components/ArtworkRail/ArtworkRailCard.tests.tsx @@ -182,14 +182,15 @@ describe("ArtworkRailCard", () => { renderWithRelay({ Artwork: () => ({ ...artwork, + saleMessage: "$120,500", sale: { ...artwork.sale, isAuction: false }, realizedPrice: null, collectorSignals, }), }) - expect(screen.getByText("Limited-Time Offer")).toBeOnTheScreen() - expect(screen.getByText("Exp. 1d 12h")).toBeOnTheScreen() + expect(screen.getByText("$120,500")).toBeOnTheScreen() + expect(screen.getByText("Offer Expires 1d 12h")).toBeOnTheScreen() }) it("doesn't show the limited-time offer signal for auction artworks", () => { diff --git a/src/app/Components/ArtworkRail/ArtworkRailCardMeta.tsx b/src/app/Components/ArtworkRail/ArtworkRailCardMeta.tsx index 98b1bfb82e6..4ad75c7795b 100644 --- a/src/app/Components/ArtworkRail/ArtworkRailCardMeta.tsx +++ b/src/app/Components/ArtworkRail/ArtworkRailCardMeta.tsx @@ -1,13 +1,13 @@ -import { Box, Flex, HeartFillIcon, HeartIcon, Text, Touchable } from "@artsy/palette-mobile" +import { Flex, HeartFillIcon, HeartIcon, Text, Touchable } from "@artsy/palette-mobile" import { ArtworkRailCardMeta_artwork$key } from "__generated__/ArtworkRailCardMeta_artwork.graphql" import { ArtworkAuctionTimer } from "app/Components/ArtworkGrids/ArtworkAuctionTimer" import { ArtworkSocialSignal } from "app/Components/ArtworkGrids/ArtworkSocialSignal" import { useSaveArtworkToArtworkLists } from "app/Components/ArtworkLists/useSaveArtworkToArtworkLists" import { ARTWORK_RAIL_TEXT_CONTAINER_HEIGHT } from "app/Components/ArtworkRail/ArtworkRailCard" +import { useMetaDataTextColor } from "app/Components/ArtworkRail/ArtworkRailUtils" +import { ArtworkSaleMessage } from "app/Components/ArtworkRail/ArtworkSaleMessage" import { HEART_ICON_SIZE } from "app/Components/constants" -import { formattedTimeLeft } from "app/Scenes/Activity/utils/formattedTimeLeft" -import { saleMessageOrBidInfo as defaultSaleMessageOrBidInfo } from "app/utils/getSaleMessgeOrBidInfo" -import { getTimer } from "app/utils/getTimer" +import { saleMessageOrBidInfo } from "app/utils/getSaleMessgeOrBidInfo" import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { ArtworkActionTrackingProps, @@ -79,33 +79,20 @@ export const ArtworkRailCardMeta: React.FC = ({ collectorSignals, } = artwork - const saleMessage = defaultSaleMessageOrBidInfo({ + const saleMessage = saleMessageOrBidInfo({ artwork, isSmallTile: true, collectorSignals: collectorSignals, auctionSignals: enableAuctionImprovementsSignals ? collectorSignals?.auction : null, }) - const partnerOfferEndAt = collectorSignals?.partnerOffer?.endAt - ? formattedTimeLeft(getTimer(collectorSignals.partnerOffer.endAt).time).timerCopy - : "" - - const primaryTextColor = dark ? "white100" : "black100" - const secondaryTextColor = dark ? "black15" : "black60" + const { primaryTextColor, secondaryTextColor } = useMetaDataTextColor({ dark }) const displayLimitedTimeOfferSignal = collectorSignals?.partnerOffer?.isAvailable && !sale?.isAuction const displayAuctionSignal = enableAuctionImprovementsSignals && sale?.isAuction - const saleInfoTextColor = - displayAuctionSignal && collectorSignals?.auction?.liveBiddingStarted - ? "blue100" - : primaryTextColor - - const saleInfoTextWeight = - displayAuctionSignal && collectorSignals?.auction?.liveBiddingStarted ? "normal" : "bold" - const onArtworkSavedOrUnSaved = (saved: boolean) => { trackEvent( artworkActionTracks.saveOrUnsaveArtwork(saved, { @@ -128,6 +115,9 @@ export const ArtworkRailCardMeta: React.FC = ({ onCompleted: onArtworkSavedOrUnSaved, }) + const displayArtworkSocialSignal = + !sale?.isAuction && !displayLimitedTimeOfferSignal && !!collectorSignals + return ( = ({ justifyContent="space-between" > - {!!displayLimitedTimeOfferSignal && ( - - - Limited-Time Offer - - - )} - - {!sale?.isAuction && !displayLimitedTimeOfferSignal && !!collectorSignals && ( - - )} - {!!lotLabel && ( Lot {lotLabel} @@ -196,28 +169,12 @@ export const ArtworkRailCardMeta: React.FC = ({ {SalePriceComponent ? SalePriceComponent : !!saleMessage && ( - - {saleMessage} - - {!!displayLimitedTimeOfferSignal && ( - - {" "} - Exp. {partnerOfferEndAt} - - )} - + )} {!!isUnlisted && ( @@ -235,6 +192,15 @@ export const ArtworkRailCardMeta: React.FC = ({ {!!displayAuctionSignal && !!collectorSignals && ( )} + + {!!displayArtworkSocialSignal && ( + + )} {!!showSaveIcon && ( @@ -290,7 +256,6 @@ const artworkMetaFragment = graphql` sale { isAuction isClosed - endAt } saleArtwork { currentBid { @@ -325,6 +290,7 @@ const artworkMetaFragment = graphql` ...ArtworkSocialSignal_collectorSignals } + ...ArtworkSaleMessage_artwork ...useSaveArtworkToArtworkLists_artwork } ` diff --git a/src/app/Components/ArtworkRail/ArtworkRailUtils.tsx b/src/app/Components/ArtworkRail/ArtworkRailUtils.tsx new file mode 100644 index 00000000000..c3a78ed4019 --- /dev/null +++ b/src/app/Components/ArtworkRail/ArtworkRailUtils.tsx @@ -0,0 +1,9 @@ +export const useMetaDataTextColor = ({ dark }: { dark: boolean }) => { + const primaryTextColor = dark ? "white100" : "black100" + + const secondaryTextColor = dark ? "black15" : "black60" + + const backgroundColor = dark ? "black100" : "white100" + + return { primaryTextColor, secondaryTextColor, backgroundColor } +} diff --git a/src/app/Components/ArtworkRail/ArtworkSaleMessage.tsx b/src/app/Components/ArtworkRail/ArtworkSaleMessage.tsx new file mode 100644 index 00000000000..9327ed97cc2 --- /dev/null +++ b/src/app/Components/ArtworkRail/ArtworkSaleMessage.tsx @@ -0,0 +1,125 @@ +import { Flex, Text, TextProps } from "@artsy/palette-mobile" +import { ArtworkSaleMessage_artwork$key } from "__generated__/ArtworkSaleMessage_artwork.graphql" +import { useMetaDataTextColor } from "app/Components/ArtworkRail/ArtworkRailUtils" +import { formattedTimeLeft } from "app/Scenes/Activity/utils/formattedTimeLeft" +import { displayAsLinethrought, parsedSaleMessage } from "app/utils/getSaleMessgeOrBidInfo" +import { getTimer } from "app/utils/getTimer" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" +import { graphql, useFragment } from "react-relay" + +interface ArtworkSaleMessageProps { + artwork: ArtworkSaleMessage_artwork$key + displayLimitedTimeOfferSignal: boolean | null | undefined + saleMessage: string | null | undefined + saleInfoTextStyle?: TextProps + dark?: boolean +} + +export const ArtworkSaleMessage: React.FC = ({ + artwork, + displayLimitedTimeOfferSignal, + saleMessage, + saleInfoTextStyle, + dark = false, +}) => { + const enableAuctionImprovementsSignals = useFeatureFlag("AREnableAuctionImprovementsSignals") + + const { collectorSignals, sale } = useFragment(fragment, artwork) + + const { primaryTextColor } = useMetaDataTextColor({ dark }) + + const displayAuctionSignal = enableAuctionImprovementsSignals && sale?.isAuction + + const partnerOfferEndAt = collectorSignals?.partnerOffer?.endAt + ? formattedTimeLeft(getTimer(collectorSignals?.partnerOffer.endAt).time).timerCopy + : "" + + const saleInfoTextColor = + displayAuctionSignal && collectorSignals?.auction?.liveBiddingStarted + ? "blue100" + : primaryTextColor + + const saleInfoTextWeight = + displayAuctionSignal && collectorSignals?.auction?.liveBiddingStarted ? "normal" : "bold" + + const { parts } = parsedSaleMessage(saleMessage) + + if (!parts) return null + + if (displayLimitedTimeOfferSignal) { + return ( + <> + + {parts.map((part, index) => { + if (displayAsLinethrought(part)) { + return ( + + {" "} + {part.slice(1, -1)} + + ) + } + return ( + + {part} + + ) + })} + + + Offer Expires {partnerOfferEndAt} + + + ) + } else + return ( + + {saleMessage} + + ) +} + +const fragment = graphql` + fragment ArtworkSaleMessage_artwork on Artwork { + collectorSignals { + partnerOffer { + endAt + } + auction { + liveBiddingStarted + } + } + sale { + isAuction + } + } +` diff --git a/src/app/Components/ContextMenu/ContextMenuArtworkPreviewCard.tsx b/src/app/Components/ContextMenu/ContextMenuArtworkPreviewCard.tsx index b3c98b47751..b4ae72c4786 100644 --- a/src/app/Components/ContextMenu/ContextMenuArtworkPreviewCard.tsx +++ b/src/app/Components/ContextMenu/ContextMenuArtworkPreviewCard.tsx @@ -1,8 +1,9 @@ import { Flex, Text, useScreenDimensions, useSpace } from "@artsy/palette-mobile" import { ContextMenuArtworkPreviewCard_artwork$key } from "__generated__/ContextMenuArtworkPreviewCard_artwork.graphql" +import { useMetaDataTextColor } from "app/Components/ArtworkRail/ArtworkRailUtils" import { ArtworkDisplayProps } from "app/Components/ContextMenu/ContextMenuArtwork" import { ContextMenuArtworkPreviewCardImage } from "app/Components/ContextMenu/ContextMenuArtworkPreviewCardImage" -import { saleMessageOrBidInfo as defaultSaleMessageOrBidInfo } from "app/utils/getSaleMessgeOrBidInfo" +import { saleMessageOrBidInfo } from "app/utils/getSaleMessgeOrBidInfo" import { getUrgencyTag } from "app/utils/getUrgencyTag" import { PixelRatio } from "react-native" import { isTablet } from "react-native-device-info" @@ -42,16 +43,14 @@ export const ContextMenuArtworkPreviewCard: React.FC { return ARTWORK_RAIL_TEXT_CONTAINER_HEIGHT diff --git a/src/app/Components/ContextMenu/ContextMenuArtworkPreviewCardImage.tsx b/src/app/Components/ContextMenu/ContextMenuArtworkPreviewCardImage.tsx index 9f88a8cf5c8..ac83587305a 100644 --- a/src/app/Components/ContextMenu/ContextMenuArtworkPreviewCardImage.tsx +++ b/src/app/Components/ContextMenu/ContextMenuArtworkPreviewCardImage.tsx @@ -1,10 +1,9 @@ -import { Flex, Text, useColor } from "@artsy/palette-mobile" +import { Flex, Image, Text, useColor } from "@artsy/palette-mobile" import { ContextMenuArtworkPreviewCardImage_artwork$key } from "__generated__/ContextMenuArtworkPreviewCardImage_artwork.graphql" import { LEGACY_ARTWORK_RAIL_CARD_IMAGE_HEIGHT, ARTWORK_RAIL_CARD_IMAGE_WIDTH, } from "app/Components/ArtworkRail/LegacyArtworkRailCardImage" -import { OpaqueImageView } from "app/Components/OpaqueImageView2" import { sizeToFit } from "app/utils/useSizeToFit" import { graphql, useFragment } from "react-relay" @@ -62,9 +61,9 @@ export const ContextMenuArtworkPreviewCardImage: React.FC< return ( - diff --git a/src/app/Scenes/Artwork/Components/ArtworkDetailsCollectorSignal.tsx b/src/app/Scenes/Artwork/Components/ArtworkDetailsCollectorSignal.tsx index 6dd5fc7dc5f..b496f14e0ba 100644 --- a/src/app/Scenes/Artwork/Components/ArtworkDetailsCollectorSignal.tsx +++ b/src/app/Scenes/Artwork/Components/ArtworkDetailsCollectorSignal.tsx @@ -1,13 +1,12 @@ import { + ArrowUpRightIcon, bullet, FairIcon, + FireIcon, Flex, LinkText, Text, - TrendingIcon, - VerifiedIcon, } from "@artsy/palette-mobile" - import { ArtworkDetailsCollectorSignal_artwork$key } from "__generated__/ArtworkDetailsCollectorSignal_artwork.graphql" import { navigate } from "app/system/navigation/navigate" import { DateTime } from "luxon" @@ -40,13 +39,13 @@ export const ArtworkDetailsCollectorSignal: React.FC = ({ artwork }) => { case curatorsPick: { singalTitle = "Curators’ Pick" signalDescription = "Hand selected by Artsy curators this week" - SignalIcon = VerifiedIcon + SignalIcon = FireIcon break } case increasedInterest: { singalTitle = "Increased Interest" signalDescription = "Based on collector activity in the past 14 days" - SignalIcon = TrendingIcon + SignalIcon = ArrowUpRightIcon break } case !!runningShow: { diff --git a/src/app/Scenes/MyCollection/Screens/ArtworkList/MyCollectionArtworkGridItem.tests.tsx b/src/app/Scenes/MyCollection/Screens/ArtworkList/MyCollectionArtworkGridItem.tests.tsx index 01946433f37..cd5b5c43ffe 100644 --- a/src/app/Scenes/MyCollection/Screens/ArtworkList/MyCollectionArtworkGridItem.tests.tsx +++ b/src/app/Scenes/MyCollection/Screens/ArtworkList/MyCollectionArtworkGridItem.tests.tsx @@ -117,7 +117,7 @@ describe("MyCollectionArtworkGridItem", () => { }), }) - expect(screen.getByTestId("test-high-demand-icon")).toBeTruthy() + expect(screen.getByTestId("test-high-demand-signal")).toBeTruthy() }) it("does not render the high demand icon if artist is P1 and artwork is submitted for sale", () => { @@ -141,6 +141,6 @@ describe("MyCollectionArtworkGridItem", () => { }), }) - expect(screen.queryByTestId("test-high-demand-icon")).toBeFalsy() + expect(screen.queryByTestId("test-high-demand-signal")).toBeFalsy() }) }) diff --git a/src/app/Scenes/MyCollection/Screens/ArtworkList/MyCollectionArtworkGridItem.tsx b/src/app/Scenes/MyCollection/Screens/ArtworkList/MyCollectionArtworkGridItem.tsx index 38d7b53405e..92a2c219939 100644 --- a/src/app/Scenes/MyCollection/Screens/ArtworkList/MyCollectionArtworkGridItem.tsx +++ b/src/app/Scenes/MyCollection/Screens/ArtworkList/MyCollectionArtworkGridItem.tsx @@ -1,8 +1,7 @@ import { tappedCollectedArtwork } from "@artsy/cohesion" -import { Flex, Box, Text, Popover, useColor } from "@artsy/palette-mobile" +import { Box, Text, Popover, useColor, TrendingIcon } from "@artsy/palette-mobile" import { MyCollectionArtworkGridItem_artwork$data } from "__generated__/MyCollectionArtworkGridItem_artwork.graphql" import { DEFAULT_SECTION_MARGIN } from "app/Components/ArtworkGrids/InfiniteScrollArtworksGrid" -import HighDemandIcon from "app/Components/Icons/HighDemandIcon" import { useSetActivePopover } from "app/Components/ProgressiveOnboarding/useSetActivePopover" import { MyCollectionImageView } from "app/Scenes/MyCollection/Components/MyCollectionImageView" import { SubmissionStatus } from "app/Scenes/MyCollection/Components/SubmissionStatus" @@ -119,12 +118,6 @@ const MyCollectionArtworkGridItem: React.FC = {artistNames} - - {!!showHighDemandIcon && ( - - - - )} {!!title ? ( @@ -142,6 +135,15 @@ const MyCollectionArtworkGridItem: React.FC = ) : null} + {!!showHighDemandIcon && ( + + + + High demand + + + )} + {!!enableSubmitArtworkTier2Information && } diff --git a/src/app/utils/getSaleMessgeOrBidInfo.ts b/src/app/utils/getSaleMessgeOrBidInfo.ts index b349af5622e..d9a6d6cb135 100644 --- a/src/app/utils/getSaleMessgeOrBidInfo.ts +++ b/src/app/utils/getSaleMessgeOrBidInfo.ts @@ -84,8 +84,24 @@ export const saleMessageOrBidInfo = ({ } if (collectorSignals?.partnerOffer?.isAvailable) { - return collectorSignals.partnerOffer.priceWithDiscount?.display + // If there is a partnerOffer we have to show the message as a strikethrough + // in order to not change the type of the returned value we mark the message with ~ + // and parse it in the parsedSaleMessage function to apply different styles to each part later + const salePrice = artwork.saleMessage ? `~${artwork.saleMessage}~` : "" + + return `${collectorSignals.partnerOffer.priceWithDiscount?.display}${salePrice}` } return artwork.saleMessage } + +export const parsedSaleMessage = (saleMessage: string | null | undefined) => { + // Split the sale message into parts to apply different styles to each part + const parts = saleMessage && saleMessage.split(/(~.*?~)/) + + return { parts } +} + +export const displayAsLinethrought = (part: string) => { + return part.startsWith("~") && part.endsWith("~") +}