Skip to content

Commit

Permalink
feat: add activity card for collector update notification (#10672)
Browse files Browse the repository at this point in the history
* feat: add activity card for collector update notification

* feat: address review requests

* feat: cleanup and organization
  • Loading branch information
araujobarret authored Aug 28, 2024
1 parent 1b27a7c commit cc7b21c
Show file tree
Hide file tree
Showing 12 changed files with 316 additions and 128 deletions.
4 changes: 2 additions & 2 deletions src/app/Scenes/Activity/ActivityContent.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { Box, Flex, Separator, SkeletonBox, Spacer } from "@artsy/palette-mobile"
import { ActivityContentQuery } from "__generated__/ActivityContentQuery.graphql"
import { useMarkNotificationsAsSeen } from "app/Scenes/Activity/hooks/useMarkNotificationsAsSeen"
import { times } from "lodash"
import { Fragment } from "react"
import { graphql, useLazyLoadQuery } from "react-relay"
import { ActivityList } from "./ActivityList"
import { NotificationType } from "./types"
import { getNotificationTypes } from "./utils/getNotificationTypes"
import { Box, Flex, Separator, SkeletonBox, Spacer } from "@artsy/palette-mobile"
import { times } from "lodash"

interface ActivityProps {
type: NotificationType
Expand Down
2 changes: 2 additions & 0 deletions src/app/Scenes/Activity/ActivityItem.tests.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ const notification = {
url: "artwork-image-four",
},
],
item: { __typename: "PartnerOfferCreatedNotificationItem" },
}

const notificationWithFF = {
Expand All @@ -287,4 +288,5 @@ const notificationWithFF = {
url: "artwork-image-four",
},
],
item: { __typename: "PartnerOfferCreatedNotificationItem" },
}
155 changes: 85 additions & 70 deletions src/app/Scenes/Activity/ActivityItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ 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 { CollectorUpdateNotification } from "app/Scenes/Activity/components/CollectorUpdateNotification"
import {
ExpiresInTimer,
shouldDisplayExpiresInTimer,
Expand All @@ -26,101 +27,108 @@ const UNREAD_INDICATOR_SIZE = 8
const NEW_ARTWORK_IMAGE_SIZE = 60

export const ActivityItem: React.FC<ActivityItemProps> = memo(
(props) => {
({ isVisible, notification: _notification }) => {
const enableBlurhash = useFeatureFlag("ARShowBlurhashImagePlaceholder")

const markAsRead = useMarkNotificationAsRead()
const tracking = useTracking()
const item = useFragment(activityItemFragment, props.notification)
const artworksCount = item.objectsCount
const notification = useFragment(activityItemFragment, _notification)
const artworksCount = notification.objectsCount
const remainingArtworksCount = artworksCount - 4
const shouldDisplayCounts =
isArtworksBasedNotification(item.notificationType) && remainingArtworksCount > 0
const isPartnerOffer = item.notificationType === "PARTNER_OFFER_CREATED"
const isEditorial = item.notificationType === "ARTICLE_FEATURED_ARTIST"
isArtworksBasedNotification(notification.notificationType) && remainingArtworksCount > 0
const isPartnerOffer = notification.notificationType === "PARTNER_OFFER_CREATED"
const isEditorial = notification.notificationType === "ARTICLE_FEATURED_ARTIST"

const handlePress = () => {
tracking.trackEvent(tracks.tappedNotification(item.notificationType))
tracking.trackEvent(tracks.tappedNotification(notification.notificationType))

if (item.isUnread) {
markAsRead(item)
if (notification.isUnread) {
markAsRead(notification)
}

navigateToActivityItem(item)
navigateToActivityItem(notification)
}

const showAsRow = isPartnerOffer

return (
<TouchableOpacity activeOpacity={0.65} onPress={handlePress}>
<Flex flexDirection="row" alignItems="center" px={2}>
<Flex flex={1} mr={2}>
<Flex flexDirection="column" py={2}>
<Flex flexDirection={showAsRow ? "row" : "column"}>
<Flex flexDirection="row" alignItems="center">
{item.previewImages.map((image) => {
if (!image?.url) return null

return (
<Flex
key={image.url}
mr={1}
mb={1}
accessibilityLabel="Activity Artwork Image"
height={NEW_ARTWORK_IMAGE_SIZE}
width={NEW_ARTWORK_IMAGE_SIZE}
>
<Image
src={image.url}
width={NEW_ARTWORK_IMAGE_SIZE}
<Flex flexDirection="row" alignItems="center" justifyContent="space-between" px={2}>
{notification.item?.__typename === "CollectorProfileUpdatePromptNotificationItem" ? (
<CollectorUpdateNotification notification={notification} item={notification.item} />
) : (
<Flex flex={1} pr={2}>
<Flex flexDirection="column" py={2}>
<Flex flexDirection={showAsRow ? "row" : "column"}>
<Flex flexDirection="row" alignItems="center">
{notification.previewImages.map((image) => {
if (!image?.url) return null

return (
<Flex
key={image.url}
mr={1}
mb={1}
accessibilityLabel="Activity Artwork Image"
height={NEW_ARTWORK_IMAGE_SIZE}
showLoadingState={!props.isVisible}
blurhash={enableBlurhash ? image.blurhash : undefined}
/>
</Flex>
)
})}

{!!shouldDisplayCounts && (
<Text
variant="xs"
color="black60"
accessibilityLabel="Remaining artworks count"
>
+ {remainingArtworksCount}
</Text>
)}
</Flex>

<Flex style={{ flex: 1 }}>
{!!isPartnerOffer && (
<PartnerOfferBadge notificationType={item.notificationType} />
)}

<Text variant="sm-display" fontWeight="bold">
{item.headline}
</Text>

{!!isEditorial && <Text variant="xs">{item.message}</Text>}

<Flex flexDirection="row" mt="1px">
<ActivityItemTypeLabel notificationType={item.notificationType} />
{!isPartnerOffer && (
<Text variant="xs" mr={0.5}>
{item.publishedAt}
width={NEW_ARTWORK_IMAGE_SIZE}
>
<Image
src={image.url}
width={NEW_ARTWORK_IMAGE_SIZE}
height={NEW_ARTWORK_IMAGE_SIZE}
showLoadingState={!isVisible}
blurhash={enableBlurhash ? image.blurhash : undefined}
/>
</Flex>
)
})}

{!!shouldDisplayCounts && (
<Text
variant="xs"
color="black60"
accessibilityLabel="Remaining artworks count"
>
+ {remainingArtworksCount}
</Text>
)}
{shouldDisplayExpiresInTimer(item.notificationType, item.item) && (
<Flex ml="1px">
<ExpiresInTimer item={item.item} />
</Flex>
</Flex>

<Flex style={{ flex: 1 }}>
{!!isPartnerOffer && (
<PartnerOfferBadge notificationType={notification.notificationType} />
)}

<Text variant="sm-display" fontWeight="bold">
{notification.headline}
</Text>

{!!isEditorial && <Text variant="xs">{notification.message}</Text>}

<Flex flexDirection="row" mt="1px">
<ActivityItemTypeLabel notificationType={notification.notificationType} />
{!isPartnerOffer && (
<Text variant="xs" mr={0.5}>
{notification.publishedAt}
</Text>
)}
{shouldDisplayExpiresInTimer(
notification.notificationType,
notification.item
) && (
<Flex ml="1px">
<ExpiresInTimer item={notification.item} />
</Flex>
)}
</Flex>
</Flex>
</Flex>
</Flex>
</Flex>
</Flex>
{!!item.isUnread && (
)}
{!!notification.isUnread && (
<Flex
width={UNREAD_INDICATOR_SIZE}
height={UNREAD_INDICATOR_SIZE}
Expand All @@ -140,6 +148,8 @@ export const ActivityItem: React.FC<ActivityItemProps> = memo(

const activityItemFragment = graphql`
fragment ActivityItem_notification on Notification {
...CollectorUpdateNotification_notification
internalID
id
title
Expand All @@ -152,6 +162,11 @@ const activityItemFragment = graphql`
objectsCount
item {
__typename
... on CollectorProfileUpdatePromptNotificationItem {
...CollectorUpdateNotification_item
__typename
}
... on PartnerOfferCreatedNotificationItem {
available
expiresAt
Expand Down
19 changes: 19 additions & 0 deletions src/app/Scenes/Activity/components/CollectorProfilePrompt.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { MyProfileEditModal_me$key } from "__generated__/MyProfileEditModal_me.graphql"
import { MyProfileEditModal } from "app/Scenes/MyProfile/MyProfileEditModal"
import { FC } from "react"

interface CompleteProfilePromptProps {
me: MyProfileEditModal_me$key
visible: boolean
onDismiss: () => void
}

export const CollectorProfilePrompt: FC<CompleteProfilePromptProps> = ({ me, ...rest }) => {
return (
<MyProfileEditModal
me={me}
message="Tell us a few more details about yourself to complete your profile."
{...rest}
/>
)
}
99 changes: 99 additions & 0 deletions src/app/Scenes/Activity/components/CollectorUpdateNotification.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import { Flex, Text, Touchable } from "@artsy/palette-mobile"
import { CollectorUpdateNotification_item$key } from "__generated__/CollectorUpdateNotification_item.graphql"
import { CollectorUpdateNotification_notification$key } from "__generated__/CollectorUpdateNotification_notification.graphql"
import { MyCollectionBottomSheetModalArtistsPrompt } from "app/Scenes/MyCollection/Components/MyCollectionBottomSheetModals/MyCollectionBottomSheetModalArtistsPrompt"
import { FC, useState } from "react"
import { graphql, useFragment } from "react-relay"
import { CollectorProfilePrompt } from "./CollectorProfilePrompt"

interface CollectorUpdateNotificationProps {
notification: CollectorUpdateNotification_notification$key
item: CollectorUpdateNotification_item$key
}

export const CollectorUpdateNotification: FC<CollectorUpdateNotificationProps> = ({
notification: _notification,
item: _item,
}) => {
const [promptVisible, setPromptVisible] = useState(false)
const notification = useFragment(NOTIFICATION_FRAGMENT, _notification)
const item = useFragment(ITEM_FRAGMENT, _item)

if (!notification || !item) {
return null
}

const hasEmptyCollection =
item.me.myCollectionInfo.artworksCount === 0 && item.me.myCollectionInfo.artistsCount === 0
const itemInfo = hasEmptyCollection ? addArtistsToCollectiontInfo : collectorProfileInfo

return (
<>
<Touchable onPress={() => setPromptVisible(true)}>
<Flex flex={1} py={2} pr={2}>
<Text variant="sm-display" fontWeight={500}>
{itemInfo.title}
</Text>
<Text variant="xs">{itemInfo.body}</Text>

<Text variant="xs" fontWeight={500}>
Artsy Message •
<Text variant="xs" fontWeight="normal">{` ${notification.publishedAt}`}</Text>
</Text>
</Flex>
</Touchable>

{itemInfo.prompt === "AddArtistsToCollection" ? (
<MyCollectionBottomSheetModalArtistsPrompt
title="Tell us about the artists in your collection."
visible={promptVisible}
onDismiss={() => setPromptVisible(true)}
/>
) : (
<CollectorProfilePrompt
me={item.me}
visible={promptVisible}
onDismiss={() => setPromptVisible(true)}
/>
)}
</>
)
}

const NOTIFICATION_FRAGMENT = graphql`
fragment CollectorUpdateNotification_notification on Notification {
publishedAt(format: "RELATIVE") @required(action: NONE)
}
`

const ITEM_FRAGMENT = graphql`
fragment CollectorUpdateNotification_item on CollectorProfileUpdatePromptNotificationItem {
me @required(action: NONE) {
...MyProfileEditModal_me
profession
location {
city
}
myCollectionInfo @required(action: NONE) {
artistsCount
artworksCount
}
}
collectorProfile @required(action: NONE) {
lastUpdatePromptAt
}
}
`

const addArtistsToCollectiontInfo = {
title: "Tell us about the artists in your collection.",
body: "Show off your collection and make a great impression.",
prompt: "AddArtistsToCollection",
}

const collectorProfileInfo = {
title: "Tell us a little bit more about you.",
body: "By completing your profile, you’re more likely to receive quick responses from galleries.",
prompt: "CollectorProfile",
}
4 changes: 4 additions & 0 deletions src/app/Scenes/Activity/components/ExpiresInTimer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { FC, useEffect, useRef, useState } from "react"
const INTERVAL = 1000

interface ExpiresInTimerProps {
// TOFIX: this should have it's own relay fragment, no prop drilling with relay data!
item: ActivityItem_notification$data["item"]
}

Expand All @@ -15,7 +16,9 @@ const WatchIcon: FC<{ fill?: string }> = ({ fill = "red100" }) => {
}

export const ExpiresInTimer: FC<ExpiresInTimerProps> = ({ item }) => {
// @ts-ignore: fix ExpiresInTimer fragment data
const expiresAt = item?.expiresAt ?? ""
// @ts-ignore: fix ExpiresInTimer fragment data
const available = item?.available ?? false

const intervalId = useRef<ReturnType<typeof setInterval> | null>(null)
Expand Down Expand Up @@ -77,5 +80,6 @@ export const shouldDisplayExpiresInTimer = (
notificationType: string,
item: ActivityItem_notification$data["item"]
) => {
// @ts-ignore: fix ExpiresInTimer fragment data
return notificationType === "PARTNER_OFFER_CREATED" && item?.expiresAt
}
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ export const PartnerOfferCreatedNotification: React.FC<PartnerOfferCreatedNotifi

<Spacer y={0.5} />

{/* @ts-ignore: fix ExpiresInTimer fragment data */}
{shouldDisplayExpiresInTimer(notificationType, item) && <ExpiresInTimer item={item} />}

<Spacer y={2} />
Expand Down
Loading

0 comments on commit cc7b21c

Please sign in to comment.