diff --git a/package.json b/package.json index d607953f603..142830eb123 100644 --- a/package.json +++ b/package.json @@ -114,6 +114,7 @@ "@braze/react-native-sdk": "11.0.0", "@expo/react-native-action-sheet": "4.0.1", "@gorhom/bottom-sheet": "5.0.5", + "@gorhom/portal": "1.0.14", "@invertase/react-native-apple-authentication": "2.1.5", "@kesha-antonov/react-native-action-cable": "1.1.4", "@ptomasroos/react-native-multi-slider": "2.2.2", diff --git a/src/app/Components/GlobalSearchInput.tests.tsx b/src/app/Components/GlobalSearchInput.tests.tsx new file mode 100644 index 00000000000..1c9580898b4 --- /dev/null +++ b/src/app/Components/GlobalSearchInput.tests.tsx @@ -0,0 +1,10 @@ +import { GlobalSearchInput } from "app/Components/GlobalSearchInput" +import { renderWithWrappers } from "app/utils/tests/renderWithWrappers" + +describe("GlobalSearchInput", () => { + it("renders the search label properly", () => { + renderWithWrappers() + + expect(/Search artists, artworks, etc/).toBeTruthy() + }) +}) diff --git a/src/app/Components/GlobalSearchInput.tsx b/src/app/Components/GlobalSearchInput.tsx new file mode 100644 index 00000000000..48cc6bafc0e --- /dev/null +++ b/src/app/Components/GlobalSearchInput.tsx @@ -0,0 +1,34 @@ +import { Flex, RoundSearchInput, Touchable } from "@artsy/palette-mobile" +import { GlobalSearchInputOverlay } from "app/Components/GlobalSearchInputOverlay" +import { SEARCH_INPUT_PLACEHOLDER } from "app/Scenes/Search/Search" +import { Fragment, useState } from "react" + +export const GlobalSearchInput: React.FC<{}> = () => { + const [isVisible, setIsVisible] = useState(false) + + return ( + + { + setIsVisible(true) + }} + > + {/* In order to make the search input behave like a button here, we wrapped it with a + Touchable and set pointerEvents to none. This will prevent the input from receiving + touch events and make sure they are being handled by the Touchable. + */} + + + + + setIsVisible(false)} /> + + ) +} diff --git a/src/app/Components/GlobalSearchInputOverlay.tsx b/src/app/Components/GlobalSearchInputOverlay.tsx new file mode 100644 index 00000000000..31eb0e9a0f5 --- /dev/null +++ b/src/app/Components/GlobalSearchInputOverlay.tsx @@ -0,0 +1,43 @@ +import { Flex, RoundSearchInput, Spacer } from "@artsy/palette-mobile" +import { Portal } from "@gorhom/portal" +import { FadeIn } from "app/Components/FadeIn" +import { SEARCH_INPUT_PLACEHOLDER } from "app/Scenes/Search/Search" +import { StyleSheet } from "react-native" +import { SafeAreaView } from "react-native-safe-area-context" + +export const GlobalSearchInputOverlay: React.FC<{ visible: boolean; hideModal: () => void }> = ({ + visible, + hideModal, +}) => { + if (!visible) { + return null + } + + return ( + + + + + { + hideModal() + }} + /> + + + + + + + ) +} diff --git a/src/app/Providers.tsx b/src/app/Providers.tsx index 7e91fb99067..30e94a972a0 100644 --- a/src/app/Providers.tsx +++ b/src/app/Providers.tsx @@ -1,5 +1,6 @@ import { Theme, Spinner, ScreenDimensionsProvider, Screen } from "@artsy/palette-mobile" import { ActionSheetProvider } from "@expo/react-native-action-sheet" +import { PortalProvider } from "@gorhom/portal" import { ArtworkListsProvider } from "app/Components/ArtworkLists/ArtworkListsContext" import { ShareSheetProvider } from "app/Components/ShareSheet/ShareSheetContext" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" @@ -58,6 +59,7 @@ export const TestProviders: React.FC<{ skipRelay?: boolean }> = ({ TrackingProvider, GlobalStoreProvider, SafeAreaProvider, + PortalProvider, ProvideScreenDimensions, // FIXME: Only use one from palette-mobile // @ts-ignore diff --git a/src/app/Scenes/HomeView/Components/HomeHeader.tsx b/src/app/Scenes/HomeView/Components/HomeHeader.tsx index b2ff142a17c..5540162c339 100644 --- a/src/app/Scenes/HomeView/Components/HomeHeader.tsx +++ b/src/app/Scenes/HomeView/Components/HomeHeader.tsx @@ -1,4 +1,5 @@ -import { ArtsyLogoBlackIcon, Flex, Box } from "@artsy/palette-mobile" +import { ArtsyLogoBlackIcon, Box, Flex } from "@artsy/palette-mobile" +import { GlobalSearchInput } from "app/Components/GlobalSearchInput" import { PaymentFailureBanner } from "app/Scenes/HomeView/Components/PaymentFailureBanner" import { GlobalStore } from "app/store/GlobalStore" import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" @@ -6,11 +7,40 @@ import { Suspense } from "react" import { ActivityIndicator } from "./ActivityIndicator" export const HomeHeader: React.FC = () => { + const enableNewSearchModal = useFeatureFlag("AREnableNewSearchModal") const showPaymentFailureBanner = useFeatureFlag("AREnablePaymentFailureBanner") const hasUnseenNotifications = GlobalStore.useAppState( (state) => state.bottomTabs.hasUnseenNotifications ) + if (enableNewSearchModal) { + return ( + + {!!showPaymentFailureBanner && ( + + + + )} + + + + + + + + + + + + ) + } + return ( <> {!!showPaymentFailureBanner && ( @@ -18,7 +48,7 @@ export const HomeHeader: React.FC = () => { )} - + diff --git a/src/app/Scenes/HomeView/HomeView.tsx b/src/app/Scenes/HomeView/HomeView.tsx index 1e13e78a4f7..c7756884143 100644 --- a/src/app/Scenes/HomeView/HomeView.tsx +++ b/src/app/Scenes/HomeView/HomeView.tsx @@ -1,5 +1,6 @@ import { ContextModule, OwnerType } from "@artsy/cohesion" import { Flex, Screen, Spinner } from "@artsy/palette-mobile" +import { PortalHost } from "@gorhom/portal" import { useFocusEffect } from "@react-navigation/native" import { HomeViewFetchMeQuery } from "__generated__/HomeViewFetchMeQuery.graphql" import { HomeViewQuery } from "__generated__/HomeViewQuery.graphql" @@ -18,6 +19,7 @@ import { searchQueryDefaultVariables } from "app/Scenes/Search/Search" import { getRelayEnvironment } from "app/system/relay/defaultEnvironment" import { useBottomTabsScrollToTop } from "app/utils/bottomTabsHelper" import { extractNodes } from "app/utils/extractNodes" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { ProvidePlaceholderContext } from "app/utils/placeholders" import { usePrefetch } from "app/utils/queryPrefetching" import { requestPushNotificationsPermission } from "app/utils/requestPushNotificationsPermission" @@ -33,6 +35,7 @@ export const homeViewScreenQueryVariables = () => ({ }) export const HomeView: React.FC = () => { + const enableNewSearchModal = useFeatureFlag("AREnableNewSearchModal") const flashlistRef = useBottomTabsScrollToTop("home") const [isRefreshing, setIsRefreshing] = useState(false) @@ -128,10 +131,20 @@ export const HomeView: React.FC = () => { }) } + const stickyHeaderProps = enableNewSearchModal + ? { + stickyHeaderHiddenOnScroll: true, + stickyHeaderIndices: [0], + } + : {} + return ( } data={sections} @@ -150,6 +163,7 @@ export const HomeView: React.FC = () => { } refreshControl={} onEndReachedThreshold={2} + {...stickyHeaderProps} /> {!!data?.me && } @@ -178,6 +192,7 @@ export const HomeViewScreen: React.FC = () => { }> + ) diff --git a/src/app/Scenes/Search/Search.tests.tsx b/src/app/Scenes/Search/Search.tests.tsx index 2aa1b3c387c..d6674d00ae5 100644 --- a/src/app/Scenes/Search/Search.tests.tsx +++ b/src/app/Scenes/Search/Search.tests.tsx @@ -17,7 +17,7 @@ describe("Search", () => { it("should render a text input with placeholder and no pills", async () => { const { env } = renderWithRelay() - const searchInput = screen.getByPlaceholderText("Search artists, artworks, galleries, etc") + const searchInput = screen.getByPlaceholderText("Search artists, artworks, etc") expect(searchInput).toBeTruthy() @@ -43,7 +43,7 @@ describe("Search", () => { it("Top pill should be selected by default", async () => { const { env } = renderWithRelay() - const searchInput = screen.getByPlaceholderText("Search artists, artworks, galleries, etc") + const searchInput = screen.getByPlaceholderText("Search artists, artworks, etc") fireEvent.changeText(searchInput, "text") @@ -56,7 +56,7 @@ describe("Search", () => { it("when clear button is pressed", async () => { const { env } = renderWithRelay() - const searchInput = screen.getByPlaceholderText("Search artists, artworks, galleries, etc") + const searchInput = screen.getByPlaceholderText("Search artists, artworks, etc") fireEvent(searchInput, "changeText", "prev value") @@ -77,7 +77,7 @@ describe("Search", () => { it("when cancel button is pressed", async () => { const { env } = renderWithRelay() - const searchInput = screen.getByPlaceholderText("Search artists, artworks, galleries, etc") + const searchInput = screen.getByPlaceholderText("Search artists, artworks, etc") fireEvent(searchInput, "changeText", "prev value") // needed to resolve the relay operation triggered for the text change @@ -98,7 +98,7 @@ describe("Search", () => { it("should render all the default pills", async () => { const { env } = renderWithRelay() - const searchInput = screen.getByPlaceholderText("Search artists, artworks, galleries, etc") + const searchInput = screen.getByPlaceholderText("Search artists, artworks, etc") fireEvent(searchInput, "changeText", "Ba") diff --git a/src/app/Scenes/Search/Search.tsx b/src/app/Scenes/Search/Search.tsx index 7cfc51b49a5..7e4d1064b25 100644 --- a/src/app/Scenes/Search/Search.tsx +++ b/src/app/Scenes/Search/Search.tsx @@ -1,14 +1,17 @@ import { ActionType, ContextModule, OwnerType } from "@artsy/cohesion" import { Spacer, Flex, Box, Screen } from "@artsy/palette-mobile" +import { PortalHost } from "@gorhom/portal" import { useNavigation } from "@react-navigation/native" import { StackScreenProps } from "@react-navigation/stack" import { SearchQuery, SearchQuery$variables } from "__generated__/SearchQuery.graphql" +import { GlobalSearchInput } from "app/Components/GlobalSearchInput" import { SearchInput } from "app/Components/SearchInput" import { SearchPills } from "app/Scenes/Search/SearchPills" import { useRefetchWhenQueryChanged } from "app/Scenes/Search/useRefetchWhenQueryChanged" import { useSearchQuery } from "app/Scenes/Search/useSearchQuery" import { ArtsyKeyboardAvoidingView } from "app/utils/ArtsyKeyboardAvoidingView" import { useBottomTabsScrollToTop } from "app/utils/bottomTabsHelper" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { Schema } from "app/utils/track" import { throttle } from "lodash" import { Suspense, useEffect, useMemo, useRef, useState } from "react" @@ -28,8 +31,7 @@ import { SEARCH_PILLS, SEARCH_THROTTLE_INTERVAL, TOP_PILL } from "./constants" import { getContextModuleByPillName } from "./helpers" import { PillType } from "./types" -const SEARCH_INPUT_PLACEHOLDER = [ - "Search artists, artworks, galleries, etc", +export const SEARCH_INPUT_PLACEHOLDER = [ "Search artists, artworks, etc", "Search artworks, etc", "Search", @@ -41,6 +43,7 @@ export const searchQueryDefaultVariables: SearchQuery$variables = { } export const Search: React.FC = () => { + const enableNewSearchModal = useFeatureFlag("AREnableNewSearchModal") const searchPillsRef = useRef(null) const [searchQuery, setSearchQuery] = useState("") const [selectedPill, setSelectedPill] = useState(TOP_PILL) @@ -130,11 +133,15 @@ export const Search: React.FC = () => { - + {enableNewSearchModal ? ( + + ) : ( + + )} {shouldStartSearching(searchQuery) && !!queryData.viewer ? ( @@ -158,11 +165,15 @@ export const Search: React.FC = () => { ) : ( - - - + {!enableNewSearchModal && ( + <> + + + + + + )} - @@ -189,13 +200,18 @@ export const SearchScreenQuery = graphql` type SearchScreenProps = StackScreenProps -export const SearchScreen: React.FC = () => ( - - }> - - - -) +export const SearchScreen: React.FC = () => { + return ( + <> + + }> + + + + + + ) +} const Scrollable = styled(ScrollView).attrs(() => ({ keyboardDismissMode: "on-drag", diff --git a/src/app/Scenes/Search/components/placeholders/SearchPlaceholder.tsx b/src/app/Scenes/Search/components/placeholders/SearchPlaceholder.tsx index ccbe99e687e..3b55bdb45af 100644 --- a/src/app/Scenes/Search/components/placeholders/SearchPlaceholder.tsx +++ b/src/app/Scenes/Search/components/placeholders/SearchPlaceholder.tsx @@ -1,7 +1,15 @@ -import { Spacer, Flex, Box, Join } from "@artsy/palette-mobile" +import { + Box, + Flex, + Join, + SEARCH_INPUT_CONTAINER_BORDER_RADIUS, + SEARCH_INPUT_CONTAINER_HEIGHT, + Spacer, +} from "@artsy/palette-mobile" import { CARD_WIDTH } from "app/Components/Home/CardRailCard" import { MAX_SHOWN_RECENT_SEARCHES, useRecentSearches } from "app/Scenes/Search/SearchModel" import { IMAGE_SIZE } from "app/Scenes/Search/components/SearchResultImage" +import { useFeatureFlag } from "app/utils/hooks/useFeatureFlag" import { PlaceholderBox, PlaceholderRaggedText, @@ -110,15 +118,28 @@ const CuratedCollectionsPlaceholder = () => { } export const SearchPlaceholder: React.FC = () => { + const enableNewSearchModal = useFeatureFlag("AREnableNewSearchModal") return ( {/* Search input */} - + {enableNewSearchModal ? ( + + ) : ( + + )} + - - + {!enableNewSearchModal && ( + <> + + + + )} diff --git a/src/app/store/config/features.ts b/src/app/store/config/features.ts index d2401873721..88a0b9aee92 100644 --- a/src/app/store/config/features.ts +++ b/src/app/store/config/features.ts @@ -306,6 +306,11 @@ export const features = { showInDevMenu: true, echoFlagKey: "AREnablePaymentFailureBanner", }, + AREnableNewSearchModal: { + description: "Enable new search modal", + readyForRelease: false, + showInDevMenu: true, + }, } satisfies { [key: string]: FeatureDescriptor } export interface DevToggleDescriptor {