From e948923007441e57c0ce6125fd11e56ac202f1ac Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Fri, 15 Sep 2023 22:37:18 +0000 Subject: [PATCH 1/6] chore(deps): update dep @artsy/cohesion from 4.146.0 to v4.147.0 --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index e3d6033de84..37be13da091 100644 --- a/package.json +++ b/package.json @@ -109,7 +109,7 @@ "@babel/plugin-transform-named-capturing-groups-regex": "Needed for compiling android with hermes enabled" }, "dependencies": { - "@artsy/cohesion": "4.146.0", + "@artsy/cohesion": "4.147.0", "@artsy/palette-mobile": "13.0.8", "@artsy/to-title-case": "1.1.0", "@expo/react-native-action-sheet": "4.0.1", diff --git a/yarn.lock b/yarn.lock index 77b19d2dc1c..3713b7ac98e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -17,10 +17,10 @@ "@jridgewell/gen-mapping" "^0.3.0" "@jridgewell/trace-mapping" "^0.3.9" -"@artsy/cohesion@4.146.0": - version "4.146.0" - resolved "https://registry.yarnpkg.com/@artsy/cohesion/-/cohesion-4.146.0.tgz#ce248fbc16902fe4e3025f07a33e30652da834ea" - integrity sha512-wdR7pMOcprwzhLMgzNCGRkNpQkqh2DFPKx5MtUJdGVRjVN8lW716K/X5m4VwJY7VrLhy95QLyDtfKEtGtHBD2A== +"@artsy/cohesion@4.147.0": + version "4.147.0" + resolved "https://registry.yarnpkg.com/@artsy/cohesion/-/cohesion-4.147.0.tgz#5990773316b31b6e6552818af74caca2ab8d8481" + integrity sha512-PIsTdzejXjhcQi+is9mnxtCS1NmYMqgIezwERtvnxZeYaevnMyIOpXCI+67hDdmvLPok2TqLXFStehq9gBmLfg== dependencies: core-js "3" From 3d151e60138f109666b0259ff7639eab5e534a73 Mon Sep 17 00:00:00 2001 From: Mounir Dhahri Date: Mon, 18 Sep 2023 19:17:16 +0200 Subject: [PATCH 2/6] fix: Revert "chore: use m1 machines in ci" [WIP] (#9280) Revert "chore: use m1 machines in ci (#9262)" This reverts commit 340afb5ad9e790882725b5b49a02555c473bc992. --- .circleci/config.yml | 14 ++++++++++---- .ruby-version | 2 +- .tool-versions | 2 +- 3 files changed, 12 insertions(+), 6 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1e5cbd98954..0b236e8351a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -4,6 +4,7 @@ orbs: node: circleci/node@5.1.0 queue: eddiewebb/queue@2.2.1 horizon: artsy/release@0.0.1 + ruby: circleci/ruby@2.1.0 commands: await-previous-builds: @@ -69,6 +70,10 @@ commands: - run: name: Compile query map command: yarn relay + install-ruby: + steps: + - ruby/install: + version: "2.7" install-gems: steps: - restore_cache: @@ -142,6 +147,7 @@ commands: - install-node-modules - run-relay-compiler - update-echo + - install-ruby - install-gems build-app-android: steps: @@ -177,7 +183,7 @@ jobs: macos: xcode: 14.1.0 - resource_class: macos.m1.medium.gen1 + resource_class: macos.x86.medium.gen2 steps: - checkout @@ -257,7 +263,7 @@ jobs: macos: xcode: 14.1.0 - resource_class: macos.m1.medium.gen1 + resource_class: macos.x86.medium.gen2 steps: - checkout @@ -307,7 +313,7 @@ jobs: macos: xcode: 14.1.0 - resource_class: macos.m1.medium.gen1 + resource_class: macos.x86.medium.gen2 steps: - attach_workspace: @@ -400,7 +406,7 @@ jobs: BUNDLE_PATH: .vendor # path to install gems and use for caching macos: xcode: 14.1.0 - resource_class: macos.m1.medium.gen1 + resource_class: macos.x86.medium.gen2 steps: - checkout - install-gems diff --git a/.ruby-version b/.ruby-version index 6a81b4c8379..a4dd9dba4fb 100644 --- a/.ruby-version +++ b/.ruby-version @@ -1 +1 @@ -2.7.8 +2.7.4 diff --git a/.tool-versions b/.tool-versions index 4711408323a..d9a2c681c55 100644 --- a/.tool-versions +++ b/.tool-versions @@ -1,3 +1,3 @@ nodejs 16.18.0 -ruby 2.7.8 +ruby 2.7.4 java zulu-11.58.15 From 4da87dc488075e6075d245ce57b7333b1a930136 Mon Sep 17 00:00:00 2001 From: Daria Kozlova Date: Mon, 18 Sep 2023 19:39:26 +0200 Subject: [PATCH 3/6] feat: add AREnableAuctionHeaderAlertCTA echoFlagKey (#9277) Co-authored-by: brainbicycle --- src/app/store/config/features.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/store/config/features.ts b/src/app/store/config/features.ts index 4902485b02e..a0d48df108c 100644 --- a/src/app/store/config/features.ts +++ b/src/app/store/config/features.ts @@ -271,8 +271,9 @@ export const features: { [key: string]: FeatureDescriptor } = { }, AREnableAuctionHeaderAlertCTA: { description: "Enable Auction Header Alert CTA", - readyForRelease: false, + readyForRelease: true, showInDevMenu: true, + echoFlagKey: "AREnableAuctionHeaderAlertCTA", }, AREnableFallbackToGeneratedAlertNames: { description: "Enable fallback to generated alert names", From a2d82d005df082f46fade691ee23be0e130fc051 Mon Sep 17 00:00:00 2001 From: Mounir Dhahri Date: Mon, 18 Sep 2023 19:45:10 +0200 Subject: [PATCH 4/6] fix: hide empty sections on artist about page (#9273) * fix: hide empty sections on artist about page * fix: broken test * chore: address review comments --------- Co-authored-by: brainbicycle --- .../Artist/ArtistAbout/ArtistAbout.tests.tsx | 12 ++-- .../Artist/ArtistAbout/ArtistAbout.tsx | 55 ++++++++++++------- 2 files changed, 42 insertions(+), 25 deletions(-) diff --git a/src/app/Components/Artist/ArtistAbout/ArtistAbout.tests.tsx b/src/app/Components/Artist/ArtistAbout/ArtistAbout.tests.tsx index cbbd7a5bb3c..72b3d908db1 100644 --- a/src/app/Components/Artist/ArtistAbout/ArtistAbout.tests.tsx +++ b/src/app/Components/Artist/ArtistAbout/ArtistAbout.tests.tsx @@ -27,9 +27,9 @@ describe("ArtistAbout", () => { describe("Biography", () => { it("is shown when the artist has metadata", () => { renderWithRelay({ - Boolean: (context) => { - if (context.name === "hasMetadata") { - return true + ArtistBlurb: () => { + return { + text: "a biography", } }, }) @@ -39,9 +39,9 @@ describe("ArtistAbout", () => { it("is hidden when the artist has metadata", () => { renderWithRelay({ - Boolean: (context) => { - if (context.name === "hasMetadata") { - return false + ArtistBlurb: () => { + return { + text: "", } }, }) diff --git a/src/app/Components/Artist/ArtistAbout/ArtistAbout.tsx b/src/app/Components/Artist/ArtistAbout/ArtistAbout.tsx index b3f22ec57ff..dd4da30da00 100644 --- a/src/app/Components/Artist/ArtistAbout/ArtistAbout.tsx +++ b/src/app/Components/Artist/ArtistAbout/ArtistAbout.tsx @@ -24,37 +24,42 @@ export const ArtistAbout: React.FC = ({ artist }) => { const isDisplayable = artist.hasMetadata || !!articles.length || !!relatedArtists.length || !!relatedGenes.length + const hasInsights = artist.hasArtistInsights.length > 0 + const hasArtistSeries = artist.hasArtistSeriesConnection?.totalCount ?? 0 > 0 + const hasShows = artist.hasArtistShows?.totalCount ?? 0 > 0 + const hasBiography = !!artist.hasBiographyBlurb?.text + const hasArticles = articles.length > 0 + const hasRelatedArtists = relatedArtists.length > 0 + const hasRelatedGenes = relatedGenes.length > 0 + return ( {isDisplayable ? ( <> }> - {!!artist.hasMetadata && ( + {!!hasBiography && ( <> )} + {!!hasInsights && } + {!!hasArtistSeries && ( + + )} + {!!hasArticles && } + {!!hasShows && } - - - - - {!!articles.length && } - - - - {!!relatedArtists.length && } - - {!!relatedGenes.length && } + {!!hasRelatedArtists && } + {!!hasRelatedGenes && } @@ -68,8 +73,20 @@ export const ArtistAbout: React.FC = ({ artist }) => { export const ArtistAboutContainer = createFragmentContainer(ArtistAbout, { artist: graphql` fragment ArtistAbout_artist on Artist { + hasArtistSeriesConnection: artistSeriesConnection(first: 1) { + totalCount + } + hasBiographyBlurb: biographyBlurb(format: PLAIN, partnerBio: false) { + text + } hasMetadata internalID + hasArtistInsights: insights { + entities + } + hasArtistShows: showsConnection(first: 1, sort: END_AT_ASC, status: "running") { + totalCount + } slug ...Biography_artist ...ArtistSeriesMoreSeries_artist From be8e66fcd0fe9738d0d1f955bb266f93c538e5c5 Mon Sep 17 00:00:00 2001 From: Ole Date: Mon, 18 Sep 2023 19:47:01 +0200 Subject: [PATCH 5/6] feat: Latest Activity Rail (#9261) * feat: Latest Activity Rail * feature flag and improvements * tracking & see all card * cleaning up * placeholder feature flag --------- Co-authored-by: brainbicycle --- .secrets.baseline | 2 +- src/app/Components/SectionTitle.tsx | 5 +- src/app/Scenes/Activity/ActivityItem.tsx | 80 +---------- .../Scenes/Activity/ActivityItemTypeLabel.tsx | 21 ++- .../mutations/useMarkNotificationAsRead.ts | 60 +++++++++ .../utils/getNotificationTypeLabel.ts | 21 +++ .../Activity/utils/navigateToActivityItem.ts | 22 +++ .../Home/Components/ActivityRail.tests.tsx | 126 ++++++++++++++++++ .../Scenes/Home/Components/ActivityRail.tsx | 119 +++++++++++++++++ .../Home/Components/ActivityRailItem.tsx | 97 ++++++++++++++ src/app/Scenes/Home/Home.tsx | 53 +++++++- src/app/Scenes/Home/homeAnalytics.ts | 39 +++++- src/app/Scenes/Home/useHomeModules.ts | 13 ++ src/app/store/config/features.ts | 6 + 14 files changed, 568 insertions(+), 96 deletions(-) create mode 100644 src/app/Scenes/Activity/mutations/useMarkNotificationAsRead.ts create mode 100644 src/app/Scenes/Activity/utils/getNotificationTypeLabel.ts create mode 100644 src/app/Scenes/Activity/utils/navigateToActivityItem.ts create mode 100644 src/app/Scenes/Home/Components/ActivityRail.tests.tsx create mode 100644 src/app/Scenes/Home/Components/ActivityRail.tsx create mode 100644 src/app/Scenes/Home/Components/ActivityRailItem.tsx diff --git a/.secrets.baseline b/.secrets.baseline index f8815a0ff08..9308054c368 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -1103,5 +1103,5 @@ } ] }, - "generated_at": "2023-09-12T15:16:43Z" + "generated_at": "2023-09-18T10:42:15Z" } diff --git a/src/app/Components/SectionTitle.tsx b/src/app/Components/SectionTitle.tsx index ed0c3e3eec9..7d38281d261 100644 --- a/src/app/Components/SectionTitle.tsx +++ b/src/app/Components/SectionTitle.tsx @@ -1,4 +1,4 @@ -import { ArrowRightIcon, Flex, SpacingUnit, useTheme, Text, TextProps } from "@artsy/palette-mobile" +import { ArrowRightIcon, Flex, SpacingUnit, Text, TextProps, useTheme } from "@artsy/palette-mobile" import { toTitleCase } from "@artsy/to-title-case" import { TouchableOpacity } from "react-native" @@ -15,6 +15,7 @@ const Wrapper: React.FC<{ onPress?(): any }> = ({ onPress, children }) => { } export const SectionTitle: React.FC<{ + fontWeight?: string title: React.ReactNode titleVariant?: TextProps["variant"] subtitle?: React.ReactNode @@ -23,6 +24,7 @@ export const SectionTitle: React.FC<{ mb?: SpacingUnit capitalized?: boolean }> = ({ + fontWeight, title, titleVariant = "sm-display", subtitle, @@ -48,6 +50,7 @@ export const SectionTitle: React.FC<{ ellipsizeMode="tail" numberOfLines={1} testID="title" + fontWeight={fontWeight} > {typeof title === "string" ? titleText : title} diff --git a/src/app/Scenes/Activity/ActivityItem.tsx b/src/app/Scenes/Activity/ActivityItem.tsx index ab9f784acfe..68f5ad55967 100644 --- a/src/app/Scenes/Activity/ActivityItem.tsx +++ b/src/app/Scenes/Activity/ActivityItem.tsx @@ -1,23 +1,14 @@ import { ActionType } from "@artsy/cohesion" import { ClickedActivityPanelNotificationItem } from "@artsy/cohesion/dist/Schema/Events/ActivityPanel" -import { Spacer, Flex, Text } from "@artsy/palette-mobile" -import { captureMessage } from "@sentry/react-native" -import { - ActivityItemMarkAsReadMutation, - ActivityItemMarkAsReadMutation$data, -} from "__generated__/ActivityItemMarkAsReadMutation.graphql" +import { Flex, Spacer, Text } from "@artsy/palette-mobile" import { ActivityItem_item$key } from "__generated__/ActivityItem_item.graphql" -import { FilterArray } from "app/Components/ArtworkFilter/ArtworkFilterHelpers" -import { ORDERED_ARTWORK_SORTS } from "app/Components/ArtworkFilter/Filters/SortOptions" import { OpaqueImageView } from "app/Components/OpaqueImageView2" -import { navigate } from "app/system/navigation/navigate" +import { useMarkNotificationAsRead } from "app/Scenes/Activity/mutations/useMarkNotificationAsRead" +import { navigateToActivityItem } from "app/Scenes/Activity/utils/navigateToActivityItem" import { extractNodes } from "app/utils/extractNodes" -import { last } from "lodash" -import { parse as parseQueryString } from "query-string" import { TouchableOpacity } from "react-native" -import { graphql, useFragment, useMutation } from "react-relay" +import { graphql, useFragment } from "react-relay" import { useTracking } from "react-tracking" -import { RecordSourceSelectorProxy } from "relay-runtime" import { ActivityItemTypeLabel } from "./ActivityItemTypeLabel" import { isArtworksBasedNotification } from "./utils/isArtworksBasedNotification" import { shouldDisplayNotificationTypeLabel } from "./utils/shouldDisplayNotificationTypeLabel" @@ -26,20 +17,11 @@ interface ActivityItemProps { item: ActivityItem_item$key } -const updater = ( - id: string, - store: RecordSourceSelectorProxy -) => { - const notification = store.get(id) - - notification?.setValue(false, "isUnread") -} - const UNREAD_INDICATOR_SIZE = 8 const ARTWORK_IMAGE_SIZE = 55 export const ActivityItem: React.FC = (props) => { - const [markAsRead] = useMutation(markNotificationAsRead) + const markAsRead = useMarkNotificationAsRead() const tracking = useTracking() const item = useFragment(activityItemFragment, props.item) const artworks = extractNodes(item.artworksConnection) @@ -48,43 +30,12 @@ export const ActivityItem: React.FC = (props) => { isArtworksBasedNotification(item.notificationType) && remainingArtworksCount > 0 const handlePress = () => { - const splittedQueryParams = item.targetHref.split("?") - const queryParams = last(splittedQueryParams) ?? "" - const parsed = parseQueryString(queryParams) - - const sortFilterItem = ORDERED_ARTWORK_SORTS.find( - (sortEntity) => sortEntity.paramValue === "-published_at" - )! - - const navigateToActivityItem = () => - navigate(item.targetHref, { - passProps: { - predefinedFilters: [sortFilterItem] as FilterArray, - searchCriteriaID: parsed.search_criteria_id, - }, - }) - tracking.trackEvent(tracks.tappedNotification(item.notificationType)) - navigateToActivityItem() + navigateToActivityItem(item.targetHref) if (item.isUnread) { - markAsRead({ - variables: { - input: { - id: item.internalID, - }, - }, - optimisticUpdater: (store) => { - updater(item.id, store) - }, - updater: (store) => { - updater(item.id, store) - }, - onError: (error) => { - captureMessage(error?.stack!) - }, - }) + markAsRead(item) } } @@ -183,20 +134,3 @@ const tracks = { notification_type: notificationType, }), } - -const markNotificationAsRead = graphql` - mutation ActivityItemMarkAsReadMutation($input: MarkNotificationAsReadInput!) { - markNotificationAsRead(input: $input) { - responseOrError { - ... on MarkNotificationAsReadSuccess { - success - } - ... on MarkNotificationAsReadFailure { - mutationError { - message - } - } - } - } - } -` diff --git a/src/app/Scenes/Activity/ActivityItemTypeLabel.tsx b/src/app/Scenes/Activity/ActivityItemTypeLabel.tsx index eb0678a44c1..5dfe6df0287 100644 --- a/src/app/Scenes/Activity/ActivityItemTypeLabel.tsx +++ b/src/app/Scenes/Activity/ActivityItemTypeLabel.tsx @@ -1,22 +1,17 @@ import { Text } from "@artsy/palette-mobile" +import { NotificationTypesEnum } from "__generated__/ActivityRail_notificationsConnection.graphql" +import { + getNotificationTypeColor, + getNotificationTypeLabel, +} from "app/Scenes/Activity/utils/getNotificationTypeLabel" interface Props { - notificationType: string + notificationType: NotificationTypesEnum } export const ActivityItemTypeLabel: React.FC = ({ notificationType }) => { - const getNotificationType = () => { - if (notificationType === "ARTWORK_ALERT") { - return "Alert" - } - if (notificationType === "ARTICLE_FEATURED_ARTIST") { - return "Artsy Editorial" - } - - return null - } - const notificationTypeLabel = getNotificationType() - const notificationTypeColor = notificationType == "ARTWORK_ALERT" ? "blue100" : "black60" + const notificationTypeLabel = getNotificationTypeLabel(notificationType) + const notificationTypeColor = getNotificationTypeColor(notificationType) return ( { + const [commitMutation] = useMutation( + graphql` + mutation useMarkNotificationAsReadMutation($input: MarkNotificationAsReadInput!) { + markNotificationAsRead(input: $input) { + responseOrError { + ... on MarkNotificationAsReadSuccess { + success + } + ... on MarkNotificationAsReadFailure { + mutationError { + message + } + } + } + } + } + ` + ) + + return (item: { id: string; internalID: string; isUnread: boolean }) => { + if (!item.isUnread) { + return + } + + commitMutation({ + variables: { + input: { + id: item.internalID, + }, + }, + optimisticUpdater: (store) => { + updater(item.id, store) + }, + updater: (store) => { + updater(item.id, store) + }, + onError: (error) => { + captureMessage(error?.stack!) + }, + }) + } +} + +const updater = ( + id: string, + store: RecordSourceSelectorProxy +) => { + const notification = store.get(id) + + notification?.setValue(false, "isUnread") +} diff --git a/src/app/Scenes/Activity/utils/getNotificationTypeLabel.ts b/src/app/Scenes/Activity/utils/getNotificationTypeLabel.ts new file mode 100644 index 00000000000..aa31346d138 --- /dev/null +++ b/src/app/Scenes/Activity/utils/getNotificationTypeLabel.ts @@ -0,0 +1,21 @@ +import { NotificationTypesEnum } from "__generated__/ActivityRail_notificationsConnection.graphql" + +export const getNotificationTypeLabel = (notificationType: NotificationTypesEnum) => { + switch (notificationType) { + case "ARTICLE_FEATURED_ARTIST": + return "Editorial" + case "ARTWORK_ALERT": + return "Alert" + default: + return null + } +} + +export const getNotificationTypeColor = (notificationType: NotificationTypesEnum) => { + switch (notificationType) { + case "ARTWORK_ALERT": + return "blue100" + default: + return "black60" + } +} diff --git a/src/app/Scenes/Activity/utils/navigateToActivityItem.ts b/src/app/Scenes/Activity/utils/navigateToActivityItem.ts new file mode 100644 index 00000000000..76137012b0a --- /dev/null +++ b/src/app/Scenes/Activity/utils/navigateToActivityItem.ts @@ -0,0 +1,22 @@ +import { FilterArray } from "app/Components/ArtworkFilter/ArtworkFilterHelpers" +import { ORDERED_ARTWORK_SORTS } from "app/Components/ArtworkFilter/Filters/SortOptions" +import { navigate } from "app/system/navigation/navigate" +import { last } from "lodash" +import { parse as parseQueryString } from "query-string" + +export const navigateToActivityItem = (targetHref: string) => { + const splittedQueryParams = targetHref.split("?") + const queryParams = last(splittedQueryParams) ?? "" + const parsed = parseQueryString(queryParams) + + const sortFilterItem = ORDERED_ARTWORK_SORTS.find( + (sortEntity) => sortEntity.paramValue === "-published_at" + )! + + navigate(targetHref, { + passProps: { + predefinedFilters: [sortFilterItem] as FilterArray, + searchCriteriaID: parsed.search_criteria_id, + }, + }) +} diff --git a/src/app/Scenes/Home/Components/ActivityRail.tests.tsx b/src/app/Scenes/Home/Components/ActivityRail.tests.tsx new file mode 100644 index 00000000000..fe61c91c885 --- /dev/null +++ b/src/app/Scenes/Home/Components/ActivityRail.tests.tsx @@ -0,0 +1,126 @@ +import { fireEvent, screen } from "@testing-library/react-native" +import { ActivityRailTestQuery } from "__generated__/ActivityRailTestQuery.graphql" +import { ActivityRail } from "app/Scenes/Home/Components/ActivityRail" +import { navigate } from "app/system/navigation/navigate" +import { mockTrackEvent } from "app/utils/tests/globallyMockedStuff" +import { setupTestWrapper } from "app/utils/tests/setupTestWrapper" +import { graphql } from "relay-runtime" + +describe("ActivityRail", () => { + const { renderWithRelay } = setupTestWrapper({ + Component: ({ viewer }) => { + return + }, + query: graphql` + query ActivityRailTestQuery @relay_test_operation { + viewer { + ...ActivityRail_notificationsConnection + } + } + `, + }) + + it("renders", () => { + renderWithRelay({ + Viewer: () => ({ + notificationsConnection: { edges: [{ node: { internalID: "id-1" } }] }, + }), + }) + + expect(screen.getByText("Latest Activity Rail")).toBeOnTheScreen() + expect(screen.getByText(/mock-value-for-field-"title"/)).toBeOnTheScreen() + }) + + it("handles header tap", () => { + renderWithRelay({ + Viewer: () => ({ + notificationsConnection: { edges: [{ node: { internalID: "id-1" } }] }, + }), + }) + + fireEvent.press(screen.getByText("Latest Activity Rail")) + + expect(navigate).toHaveBeenCalledWith("/activity") + + expect(mockTrackEvent).toHaveBeenCalledWith({ + action: "tappedActivityGroup", + context_module: "activityRail", + context_screen_owner_id: undefined, + context_screen_owner_slug: undefined, + context_screen_owner_type: "home", + destination_screen_owner_id: undefined, + destination_screen_owner_slug: undefined, + destination_screen_owner_type: "activities", + horizontal_slide_position: undefined, + module_height: undefined, + type: "header", + }) + }) + + it("handles See All tap", () => { + renderWithRelay({ + Viewer: () => ({ + notificationsConnection: { edges: [{ node: { internalID: "id-1" } }] }, + }), + }) + + fireEvent.press(screen.getByText("See All")) + + expect(navigate).toHaveBeenCalledWith("/activity") + + expect(mockTrackEvent).toHaveBeenCalledWith({ + action: "tappedActivityGroup", + context_module: "activityRail", + context_screen_owner_id: undefined, + context_screen_owner_slug: undefined, + context_screen_owner_type: "home", + destination_screen_owner_id: undefined, + destination_screen_owner_slug: undefined, + destination_screen_owner_type: "activities", + horizontal_slide_position: undefined, + module_height: undefined, + type: "viewAll", + }) + }) + + it("handles item tap", () => { + renderWithRelay({ + Viewer: () => ({ + notificationsConnection: { edges: [{ node: { internalID: "id-1" } }] }, + }), + }) + + fireEvent.press(screen.getByText(/mock-value-for-field-"title"/)) + + expect(navigate).toHaveBeenCalledWith('', { + passProps: { + predefinedFilters: [ + { displayText: "Recently Added", paramName: "sort", paramValue: "-published_at" }, + ], + searchCriteriaID: undefined, + }, + }) + + expect(mockTrackEvent).toHaveBeenCalledWith({ + action: "tappedActivityGroup", + context_module: "activityRail", + context_screen_owner_type: "home", + destination_screen_owner_type: "vanityurlentity", + horizontal_slide_position: 0, + module_height: "single", + type: "thumbnail", + }) + }) + + describe("when there are no notifications", () => { + it("does not render", () => { + renderWithRelay({ + Viewer: () => ({ + notificationsConnection: { edges: [] }, + }), + }) + + expect(screen.queryByText("Latest Activity Rail")).not.toBeOnTheScreen() + }) + }) +}) diff --git a/src/app/Scenes/Home/Components/ActivityRail.tsx b/src/app/Scenes/Home/Components/ActivityRail.tsx new file mode 100644 index 00000000000..4403f14f78d --- /dev/null +++ b/src/app/Scenes/Home/Components/ActivityRail.tsx @@ -0,0 +1,119 @@ +import { Flex, Spacer, Text, useTheme } from "@artsy/palette-mobile" +import { ActivityRail_notificationsConnection$key } from "__generated__/ActivityRail_notificationsConnection.graphql" +import { SectionTitle } from "app/Components/SectionTitle" +import { isArtworksBasedNotification } from "app/Scenes/Activity/utils/isArtworksBasedNotification" +import { ActivityRailItem } from "app/Scenes/Home/Components/ActivityRailItem" +import HomeAnalytics from "app/Scenes/Home/homeAnalytics" +import { matchRoute } from "app/routes" +import { navigate } from "app/system/navigation/navigate" +import { extractNodes } from "app/utils/extractNodes" +import { FlatList, TouchableOpacity } from "react-native" +import { graphql, useFragment } from "react-relay" +import { useTracking } from "react-tracking" + +interface ActivityRailProps { + title: string + notificationsConnection: ActivityRail_notificationsConnection$key | null +} + +export const ActivityRail: React.FC = ({ title, notificationsConnection }) => { + const { trackEvent } = useTracking() + + const data = useFragment(notificationsConnectionFragment, notificationsConnection) + + const notificationsNodes = extractNodes(data?.notificationsConnection) + + const notifications = notificationsNodes.filter((notification) => { + if (isArtworksBasedNotification(notification.notificationType)) { + const artworksCount = notification.artworks?.totalCount ?? 0 + return artworksCount > 0 + } + + return true + }) + + if (notifications.length === 0) { + return null + } + + const handleHeaderPress = () => { + trackEvent(HomeAnalytics.activityHeaderTapEvent()) + + navigate("/activity") + } + + const handleMorePress = () => { + trackEvent(HomeAnalytics.activityViewAllTapEvent()) + + navigate("/activity") + } + + return ( + + + + + + } + ListFooterComponent={() => } + ItemSeparatorComponent={() => } + data={notifications} + keyExtractor={(item) => item.internalID} + renderItem={({ item, index }) => ( + { + const destinationRoute = matchRoute(item.targetHref) + const destinationModule = + destinationRoute.type === "match" ? destinationRoute?.module : "" + + trackEvent(HomeAnalytics.activityThumbnailTapEvent(index, destinationModule)) + }} + /> + )} + /> + + ) +} + +const notificationsConnectionFragment = graphql` + fragment ActivityRail_notificationsConnection on Viewer + @argumentDefinitions(count: { type: "Int", defaultValue: 6 }) { + notificationsConnection(first: $count) { + edges { + node { + internalID + notificationType + artworks: artworksConnection { + totalCount + } + ...ActivityRailItem_item + } + } + } + } +` + +interface SeeAllCardProps { + onPress: () => void +} + +export const SeeAllCard: React.FC = ({ onPress }) => { + const { space } = useTheme() + + return ( + + + + See All + + + + ) +} diff --git a/src/app/Scenes/Home/Components/ActivityRailItem.tsx b/src/app/Scenes/Home/Components/ActivityRailItem.tsx new file mode 100644 index 00000000000..3293248273b --- /dev/null +++ b/src/app/Scenes/Home/Components/ActivityRailItem.tsx @@ -0,0 +1,97 @@ +import { Flex, Text } from "@artsy/palette-mobile" +import { + ActivityRailItem_item$data, + ActivityRailItem_item$key, +} from "__generated__/ActivityRailItem_item.graphql" +import { OpaqueImageView } from "app/Components/OpaqueImageView2" +import { useMarkNotificationAsRead } from "app/Scenes/Activity/mutations/useMarkNotificationAsRead" +import { getNotificationTypeLabel } from "app/Scenes/Activity/utils/getNotificationTypeLabel" +import { navigateToActivityItem } from "app/Scenes/Activity/utils/navigateToActivityItem" +import { extractNodes } from "app/utils/extractNodes" +import { TouchableOpacity } from "react-native" +import { graphql, useFragment } from "react-relay" + +interface ActivityRailItemProps { + item: ActivityRailItem_item$key + onPress?: (item: ActivityRailItem_item$data) => void +} + +export const ACTIVITY_RAIL_ARTWORK_IMAGE_SIZE = 55 +const MAX_WIDTH = 220 + +export const ActivityRailItem: React.FC = (props) => { + const markAsRead = useMarkNotificationAsRead() + + const item = useFragment(ActivityRailItemFragment, props.item) + const artworks = extractNodes(item.artworksConnection) + + const handlePress = () => { + props.onPress?.(item) + + markAsRead(item) + + navigateToActivityItem(item.targetHref) + } + + const imageURL = artworks[0].image?.preview?.src + + const notificationTypeLabel = getNotificationTypeLabel(item.notificationType) + + return ( + + + + + + + + + {!!notificationTypeLabel && ( + + {notificationTypeLabel}{" "} + + )} + {item.publishedAt} + + + + {item.title} + + {item.message} + + + + ) +} + +const ActivityRailItemFragment = graphql` + fragment ActivityRailItem_item on Notification { + internalID + id + title + message + publishedAt(format: "RELATIVE") + targetHref + isUnread + notificationType + objectsCount + artworksConnection(first: 1) { + edges { + node { + internalID + title + image { + aspectRatio + preview: cropped(width: 116, height: 116, version: "normalized") { + src + } + } + } + } + } + } +` diff --git a/src/app/Scenes/Home/Home.tsx b/src/app/Scenes/Home/Home.tsx index 5043f67f31e..6aaf856791f 100644 --- a/src/app/Scenes/Home/Home.tsx +++ b/src/app/Scenes/Home/Home.tsx @@ -21,6 +21,7 @@ import { Home_homePageBelow$data } from "__generated__/Home_homePageBelow.graphq import { Home_meAbove$data } from "__generated__/Home_meAbove.graphql" import { Home_meBelow$data } from "__generated__/Home_meBelow.graphql" import { Home_newWorksForYou$data } from "__generated__/Home_newWorksForYou.graphql" +import { Home_notificationsConnection$data } from "__generated__/Home_notificationsConnection.graphql" import { SearchQuery } from "__generated__/SearchQuery.graphql" import { AboveTheFoldFlatList } from "app/Components/AboveTheFoldFlatList" import { LargeArtworkRailPlaceholder } from "app/Components/ArtworkRail/LargeArtworkRail" @@ -29,6 +30,8 @@ import { RecommendedArtistsRailFragmentContainer } from "app/Components/Home/Art import { LotsByFollowedArtistsRailContainer } from "app/Components/LotsByArtistsYouFollowRail/LotsByFollowedArtistsRail" import { useDismissSavedArtwork } from "app/Components/ProgressiveOnboarding/useDismissSavedArtwork" import { ActivityIndicator } from "app/Scenes/Home/Components/ActivityIndicator" +import { ActivityRail } from "app/Scenes/Home/Components/ActivityRail" +import { ACTIVITY_RAIL_ARTWORK_IMAGE_SIZE } from "app/Scenes/Home/Components/ActivityRailItem" import { ArticlesRailFragmentContainer } from "app/Scenes/Home/Components/ArticlesRail" import { ArtworkModuleRailFragmentContainer } from "app/Scenes/Home/Components/ArtworkModuleRail" import { ArtworkRecommendationsRail } from "app/Scenes/Home/Components/ArtworkRecommendationsRail" @@ -122,6 +125,7 @@ export interface HomeProps extends ViewProps { homePageAbove: Home_homePageAbove$data | null homePageBelow: Home_homePageBelow$data | null newWorksForYou: Home_newWorksForYou$data | null + notificationsConnection: Home_notificationsConnection$data | null loading: boolean meAbove: Home_meAbove$data | null meBelow: Home_meBelow$data | null @@ -309,6 +313,8 @@ const Home = memo((props: HomeProps) => { ) case "galleriesForYouBanner": return + case "activity": + return case "lotsByFollowedArtists": return ( { const HomePlaceholder: React.FC = () => { const randomValue = useMemoizedRandom() + const enableLatestActivityRail = useFeatureFlag("AREnableLatestActivityRail") return ( @@ -631,17 +647,41 @@ const HomePlaceholder: React.FC = () => { - { - // Small tiles to mimic the artwork rails + {/* Activity Rail */} + {!!enableLatestActivityRail && ( - + }> + {times(3 + randomValue * 10).map((index) => ( + + + + + + + + + ))} + + + - } + )} + {/* Small tiles to mimic the artwork rails */} + + + + + + + {/* Larger tiles to mimic the artist rails */} @@ -662,7 +702,6 @@ const HomePlaceholder: React.FC = () => { - @@ -758,6 +797,9 @@ export const HomeQueryRenderer: React.FC = ({ environment }) => { newWorksForYou: viewer @optionalField { ...Home_newWorksForYou } + notificationsConnection: viewer @optionalField { + ...Home_notificationsConnection + } heroUnitsConnection(first: 10, private: false) @optionalField { ...Home_heroUnits ...HeroUnitsRail_heroUnitsConnection @@ -808,6 +850,7 @@ export const HomeQueryRenderer: React.FC = ({ environment }) => { homePageAbove={above.homePage} homePageBelow={below ? below.homePage : null} newWorksForYou={above.newWorksForYou} + notificationsConnection={above.notificationsConnection} meAbove={above.me} meBelow={below ? below.me : null} loading={!below} diff --git a/src/app/Scenes/Home/homeAnalytics.ts b/src/app/Scenes/Home/homeAnalytics.ts index dcef19761f9..f77c7b171a7 100644 --- a/src/app/Scenes/Home/homeAnalytics.ts +++ b/src/app/Scenes/Home/homeAnalytics.ts @@ -1,12 +1,13 @@ import { ActionType, ContextModule, + ItemViewed, OwnerType, - TappedEntityGroup, - tappedEntityGroup, RailViewed, - ItemViewed, Screen, + TappedEntityDestinationType, + TappedEntityGroup, + tappedEntityGroup, } from "@artsy/cohesion" import { ArtworkModuleRail_rail$data } from "__generated__/ArtworkModuleRail_rail.graphql" @@ -86,6 +87,38 @@ export default class HomeAnalytics { }) } + // Activity events + + static activityHeaderTapEvent(): TappedEntityGroup { + return tappedEntityGroup({ + contextModule: ContextModule.activityRail, + contextScreenOwnerType: OwnerType.home, + destinationScreenOwnerType: OwnerType.activities, + type: "header", + }) + } + + static activityViewAllTapEvent(): TappedEntityGroup { + return tappedEntityGroup({ + contextModule: ContextModule.activityRail, + contextScreenOwnerType: OwnerType.home, + destinationScreenOwnerType: OwnerType.activities, + type: "viewAll", + }) + } + + static activityThumbnailTapEvent(index: number, destinationModule: string): TappedEntityGroup { + return { + action: ActionType.tappedActivityGroup, + context_module: ContextModule.activityRail, + context_screen_owner_type: OwnerType.home, + destination_screen_owner_type: destinationModule.toLowerCase() as TappedEntityDestinationType, + horizontal_slide_position: index, + module_height: "single", + type: "thumbnail", + } + } + // Article events static articleThumbnailTapEvent( diff --git a/src/app/Scenes/Home/useHomeModules.ts b/src/app/Scenes/Home/useHomeModules.ts index cec3207ecf6..980f7a1171a 100644 --- a/src/app/Scenes/Home/useHomeModules.ts +++ b/src/app/Scenes/Home/useHomeModules.ts @@ -9,6 +9,7 @@ import { isEmpty } from "lodash" import { useMemo } from "react" export const useHomeModules = (props: HomeProps) => { + const enableLatestActivityRail = useFeatureFlag("AREnableLatestActivityRail") const enableGalleriesForYou = useFeatureFlag("AREnableGalleriesForYou") const enableCuratorsPickRail = useFeatureFlag("AREnableCuratorsPickRail") const enableDoMoreOnArtsyRail = useFeatureFlag("AREnableDoMoreOnArtsyRail") @@ -17,6 +18,17 @@ export const useHomeModules = (props: HomeProps) => { return useMemo(() => { const allModules: Array = [ // Above-The-Fold Modules + { + contextModule: ContextModule.activityRail, + contextScreen: "home", + contextScreenOwnerType: OwnerType.home, + data: props.notificationsConnection, + isEmpty: isEmpty(props.notificationsConnection), + key: "latestActivityRail", + title: "Latest Activity", + type: "activity", + hidden: !enableLatestActivityRail, + }, { contextModule: ContextModule.newWorksForYouRail, contextScreen: "home", @@ -236,6 +248,7 @@ export const useHomeModules = (props: HomeProps) => { props.homePageBelow?.recentlyViewedWorksArtworkModule, props.homePageBelow?.similarToRecentlyViewedArtworkModule, props.featured, + props.notificationsConnection, props.homePageBelow?.fairsModule, enableCuratorsPickRail, enableDoMoreOnArtsyRail, diff --git a/src/app/store/config/features.ts b/src/app/store/config/features.ts index a0d48df108c..d492bbe3a27 100644 --- a/src/app/store/config/features.ts +++ b/src/app/store/config/features.ts @@ -293,6 +293,12 @@ export const features: { [key: string]: FeatureDescriptor } = { showInDevMenu: true, echoFlagKey: "ARShowCreateAlertInArtistArtworksListFooter", }, + AREnableLatestActivityRail: { + description: "Enable Latest Activity Rail", + readyForRelease: false, + showInDevMenu: true, + echoFlagKey: "AREnableLatestActivityRail", + }, } export interface DevToggleDescriptor { From a68106cb6f6a15fd93f43302cca6b36873f642ef Mon Sep 17 00:00:00 2001 From: Ole Date: Mon, 18 Sep 2023 19:47:38 +0200 Subject: [PATCH 6/6] fix: My Collection onboarding tooltip flow direction (#9276) * fix: My Collection onboarding tooltip flow direction * version --------- Co-authored-by: brainbicycle --- package.json | 2 +- .../MyCollection/Components/MyCollectionStickyHeader.tsx | 1 + yarn.lock | 8 ++++---- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 37be13da091..a539edacb34 100644 --- a/package.json +++ b/package.json @@ -110,7 +110,7 @@ }, "dependencies": { "@artsy/cohesion": "4.147.0", - "@artsy/palette-mobile": "13.0.8", + "@artsy/palette-mobile": "v13.0.10", "@artsy/to-title-case": "1.1.0", "@expo/react-native-action-sheet": "4.0.1", "@gorhom/bottom-sheet": "4.4.5", diff --git a/src/app/Scenes/MyCollection/Components/MyCollectionStickyHeader.tsx b/src/app/Scenes/MyCollection/Components/MyCollectionStickyHeader.tsx index 8f0f4394806..f574559ad8e 100644 --- a/src/app/Scenes/MyCollection/Components/MyCollectionStickyHeader.tsx +++ b/src/app/Scenes/MyCollection/Components/MyCollectionStickyHeader.tsx @@ -173,6 +173,7 @@ export const MainStickyHeader: React.FC<{ hasArtworks: boolean }> = ({ hasArtwor !showVisualClue("MyCollectionArtistsCollectedOnboardingTooltip1") && !showVisualClue("MyCollectionArtistsCollectedOnboarding") } + flowDirection="LEFT" initialToolTipText="Tap to add more artists or artworks" position="BOTTOM" tapToDismiss diff --git a/yarn.lock b/yarn.lock index 3713b7ac98e..9fbf6ba2490 100644 --- a/yarn.lock +++ b/yarn.lock @@ -24,10 +24,10 @@ dependencies: core-js "3" -"@artsy/palette-mobile@13.0.8": - version "13.0.8" - resolved "https://registry.yarnpkg.com/@artsy/palette-mobile/-/palette-mobile-13.0.8.tgz#a5bed4563cc04a3f76ec18280bff219671a6c5ba" - integrity sha512-0WwG/xByx/78EuV3tUQfh61ogQ9wBwZTtfzNOFVCYGAZpZS4FwctJ7kT2xohTUNrPjAxdgC+smOLokwYEXS4Gg== +"@artsy/palette-mobile@v13.0.10": + version "13.0.10" + resolved "https://registry.yarnpkg.com/@artsy/palette-mobile/-/palette-mobile-13.0.10.tgz#a570709a112a0c2d769ff457302c2dcb386def31" + integrity sha512-YfMaus7MDj/yv7Gv/ZjGM0clxDOmhoyG2e+FU0ikSdsvpb5h7e3rc0GDVz+a/yrU5hoyP1ufu4IoTTjVHTGRNQ== dependencies: "@artsy/palette-tokens" "^5.0.0" "@shopify/flash-list" "^1.5.0"