Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: prepare for new search modal #11033

Merged
merged 15 commits into from
Nov 18, 2024
Merged
9 changes: 9 additions & 0 deletions src/app/AppRegistry.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ import { RecentlyViewedScreen } from "app/Scenes/RecentlyViewed/RecentlyViewed"
import { SavedArtworks } from "app/Scenes/SavedArtworks/SavedArtworks"
import { AlertArtworks } from "app/Scenes/SavedSearchAlert/AlertArtworks"
import { SearchScreen, SearchScreenQuery } from "app/Scenes/Search/Search"
import { SearchModalScreen } from "app/Scenes/SearchModal/SeachModal"
import { SubmitArtworkForm } from "app/Scenes/SellWithArtsy/ArtworkForm/SubmitArtworkForm"
import { SubmitArtworkFormEditContainer } from "app/Scenes/SellWithArtsy/ArtworkForm/SubmitArtworkFormEdit"
import { SimilarToRecentlyViewedScreen } from "app/Scenes/SimilarToRecentlyViewed/SimilarToRecentlyViewed"
Expand Down Expand Up @@ -802,6 +803,14 @@ export const modules = defineModules({
{ isRootViewForTabName: "search", hidesBackButton: true, fullBleed: true },
[SearchScreenQuery]
),
SearchModal: reactModule(SearchModalScreen, {
fullBleed: true,
hidesBackButton: true,
alwaysPresentModally: true,
screenOptions: {
animation: "fade",
},
}),
Show: reactModule(ShowQueryRenderer, { fullBleed: true }),
ShowMoreInfo: reactModule(ShowMoreInfoQueryRenderer),
SimilarToRecentlyViewed: reactModule(SimilarToRecentlyViewedScreen, {
Expand Down
30 changes: 30 additions & 0 deletions src/app/Components/GlobalSearchInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Flex, RoundSearchInput, Touchable } from "@artsy/palette-mobile"
import { GlobalSearchInputModal } from "app/Components/GlobalSearchInputModal"
import { SEARCH_INPUT_PLACEHOLDER } from "app/Scenes/Search/Search"
import { navigate } from "app/system/navigation/navigate"
import { Fragment, useState } from "react"

export const GlobalSearchInput: React.FC<{}> = () => {
const [isVisible, setIsVisible] = useState(false)
return (
MounirDhahri marked this conversation as resolved.
Show resolved Hide resolved
<Fragment>
<Touchable
onPress={() => {
navigate("search/modal")
}}
>
<Flex pointerEvents="none">
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

just out of curiosity: why do we need pointerEvents="none" here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

here, this was my solution to avoid building a search input button that looks like the input. So I made the input clickable, but to avoid focusing inside it, I wrapped it with a flex so it doesn't send touch events to its children. And voila, a search input button that's clickable but not focusable

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great idea! 🌟

A short comment in the code to explain what this Flex container does could be good..

<RoundSearchInput
placeholder={SEARCH_INPUT_PLACEHOLDER}
accessibilityHint="Search artists, artworks, galleries etc."
accessibilityLabel="Search artists, artworks, galleries etc."
maxLength={55}
numberOfLines={1}
multiline={false}
/>
</Flex>
</Touchable>
<GlobalSearchInputModal visible={isVisible} hideModal={() => setIsVisible(false)} />
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't fully understand why there is a GlobalSearchInputModal and a SearchModalScreen 🤔

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point - I am actually still trying to figure out the best way to present this modal.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overlay, modal or a modally presented screen.

  • The overlay would be nice but it would have to be injected on the HomeView as an absolute positioned view covering everything
  • The modal would be perfect, but navigating to an item would require dismissing the modal then, navigating and the transition won't look nice
  • having this as a modally presented screen would be idea in this case, however, since it's a modally presented screen, the transition didn't look nice to me

I am checking all options currently again and I am leaning towards the first solution. I know it's what we spoke about during the refinement but I wanted to also explore other approaches

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you very much for explaining 🙏

Intuitively, the first solution sounds like the best solution to me as well.

If using an absolute positioned view covering the home screen does not work, an alternative could be to hide the screen content (e.g., by setting its height to 0).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I found a way, I did exactly that but without needing to add a store value to show/hide the overlay view on both search and home views. And also without need for prop drilling.
I noticed that we are using react-native-portalas a peer dependency in yarn.lock but we were not importing it anywhere. So I specified that and used it. The API is so straightforward and it worked great

</Fragment>
)
}
32 changes: 32 additions & 0 deletions src/app/Components/GlobalSearchInputModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { Flex, RoundSearchInput, Spacer } from "@artsy/palette-mobile"
import { SEARCH_INPUT_PLACEHOLDER } from "app/Scenes/Search/Search"
import { Modal } from "react-native"
import { SafeAreaView } from "react-native-safe-area-context"

export const GlobalSearchInputModal: React.FC<{ visible: boolean; hideModal: () => void }> = ({
visible,
hideModal,
}) => {
return (
<Modal visible={visible} animationType="fade">
<SafeAreaView style={{ flex: 1 }} edges={["top", "left", "right"]}>
<Flex px={2} pt={2}>
<RoundSearchInput
placeholder={SEARCH_INPUT_PLACEHOLDER}
accessibilityHint="Search artists, artworks, galleries etc."
accessibilityLabel="Search artists, artworks, galleries etc."
maxLength={55}
numberOfLines={1}
autoFocus
multiline={false}
onLeftIconPress={() => {
hideModal()
}}
/>
</Flex>
<Spacer y={2} />
<Flex flex={1} backgroundColor="black10" />
</SafeAreaView>
</Modal>
)
}
34 changes: 32 additions & 2 deletions src/app/Scenes/HomeView/Components/HomeHeader.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,54 @@
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"
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 (
<Flex backgroundColor="white100">
{!!showPaymentFailureBanner && (
<Suspense fallback={null}>
<PaymentFailureBanner />
</Suspense>
)}
<Flex py={2}>
<Flex
flexDirection="row"
px={2}
gap={2}
justifyContent="space-around"
alignItems="center"
>
<Flex flex={1}>
<GlobalSearchInput />
</Flex>
<Flex alignItems="flex-end">
<ActivityIndicator hasUnseenNotifications={hasUnseenNotifications} />
</Flex>
</Flex>
</Flex>
</Flex>
)
}

return (
<>
{!!showPaymentFailureBanner && (
<Suspense fallback={null}>
<PaymentFailureBanner />
</Suspense>
)}
<Box py={2}>
<Box py={2} backgroundColor="white100">
<Flex flexDirection="row" px={2} justifyContent="space-between" alignItems="center">
<Box flex={1} />
<Box>
Expand Down
13 changes: 13 additions & 0 deletions src/app/Scenes/HomeView/HomeView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,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"
Expand All @@ -33,6 +34,7 @@ export const homeViewScreenQueryVariables = () => ({
})

export const HomeView: React.FC = () => {
const enableNewSearchModal = useFeatureFlag("AREnableNewSearchModal")
const flashlistRef = useBottomTabsScrollToTop("home")
const [isRefreshing, setIsRefreshing] = useState(false)

Expand Down Expand Up @@ -128,10 +130,20 @@ export const HomeView: React.FC = () => {
})
}

const stickyHeaderProps = enableNewSearchModal
? {
stickyHeaderHiddenOnScroll: true,
stickyHeaderIndices: [0],
}
: {}

return (
<Screen safeArea={true}>
<Screen.Body fullwidth>
<FlatList
automaticallyAdjustKeyboardInsets
keyboardDismissMode="on-drag"
keyboardShouldPersistTaps="handled"
showsVerticalScrollIndicator={false}
ref={flashlistRef as RefObject<FlatList>}
data={sections}
Expand All @@ -150,6 +162,7 @@ export const HomeView: React.FC = () => {
}
refreshControl={<RefreshControl refreshing={isRefreshing} onRefresh={handleRefresh} />}
onEndReachedThreshold={2}
{...stickyHeaderProps}
/>
{!!data?.me && <EmailConfirmationBannerFragmentContainer me={data.me} />}
</Screen.Body>
Expand Down
48 changes: 30 additions & 18 deletions src/app/Scenes/Search/Search.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@ import { Spacer, Flex, Box, Screen } from "@artsy/palette-mobile"
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"
Expand All @@ -28,8 +30,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",
Expand All @@ -41,6 +42,7 @@ export const searchQueryDefaultVariables: SearchQuery$variables = {
}

export const Search: React.FC = () => {
const enableNewSearchModal = useFeatureFlag("AREnableNewSearchModal")
const searchPillsRef = useRef<ScrollView>(null)
const [searchQuery, setSearchQuery] = useState<string>("")
const [selectedPill, setSelectedPill] = useState<PillType>(TOP_PILL)
Expand Down Expand Up @@ -130,11 +132,15 @@ export const Search: React.FC = () => {
<SearchContext.Provider value={searchProviderValues}>
<ArtsyKeyboardAvoidingView>
<Flex p={2} pb={0}>
<SearchInput
ref={searchProviderValues?.inputRef}
placeholder={SEARCH_INPUT_PLACEHOLDER}
onChangeText={onSearchTextChanged}
/>
{enableNewSearchModal ? (
<GlobalSearchInput />
) : (
<SearchInput
ref={searchProviderValues?.inputRef}
placeholder={SEARCH_INPUT_PLACEHOLDER}
onChangeText={onSearchTextChanged}
/>
)}
</Flex>
<Flex flex={1} collapsable={false}>
{shouldStartSearching(searchQuery) && !!queryData.viewer ? (
Expand All @@ -158,11 +164,15 @@ export const Search: React.FC = () => {
</>
) : (
<Scrollable ref={scrollableRef}>
<HorizontalPadding>
<RecentSearches />
</HorizontalPadding>
{!enableNewSearchModal && (
<>
<HorizontalPadding>
<RecentSearches />
</HorizontalPadding>
<Spacer y={4} />
</>
)}

<Spacer y={4} />
<TrendingArtists data={queryData} mb={4} />
<CuratedCollections collections={queryData} mb={4} />

Expand All @@ -189,13 +199,15 @@ export const SearchScreenQuery = graphql`

type SearchScreenProps = StackScreenProps<any>

export const SearchScreen: React.FC<SearchScreenProps> = () => (
<Screen>
<Suspense fallback={<SearchPlaceholder />}>
<Search />
</Suspense>
</Screen>
)
export const SearchScreen: React.FC<SearchScreenProps> = () => {
return (
<Screen>
<Suspense fallback={<SearchPlaceholder />}>
<Search />
</Suspense>
</Screen>
)
}

const Scrollable = styled(ScrollView).attrs(() => ({
keyboardDismissMode: "on-drag",
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
Expand Down Expand Up @@ -110,15 +118,28 @@ const CuratedCollectionsPlaceholder = () => {
}

export const SearchPlaceholder: React.FC = () => {
const enableNewSearchModal = useFeatureFlag("AREnableNewSearchModal")
return (
<ProvidePlaceholderContext>
<Box m={2} mb={0} testID="search-placeholder">
{/* Search input */}
<PlaceholderBox height={50} />
{enableNewSearchModal ? (
<PlaceholderBox
height={SEARCH_INPUT_CONTAINER_HEIGHT}
borderRadius={SEARCH_INPUT_CONTAINER_BORDER_RADIUS}
/>
) : (
<PlaceholderBox height={50} />
)}

<Spacer y={2} />

<RecentSearchesPlaceholder />
<Spacer y={4} />
{!enableNewSearchModal && (
<>
<RecentSearchesPlaceholder />
<Spacer y={4} />
</>
)}

<TrendingArtistPlaceholder />
<Spacer y={4} />
Expand Down
10 changes: 10 additions & 0 deletions src/app/Scenes/SearchModal/SeachModal.tests.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { SearchModalScreen } from "app/Scenes/SearchModal/SeachModal"
import { renderWithWrappers } from "app/utils/tests/renderWithWrappers"

describe("SeachModal", () => {
it("renders the search label properly", () => {
renderWithWrappers(<SearchModalScreen />)

expect(/Search artists, artworks, etc/).toBeTruthy()
})
})
27 changes: 27 additions & 0 deletions src/app/Scenes/SearchModal/SeachModal.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { Flex, RoundSearchInput, Screen, Spacer } from "@artsy/palette-mobile"
import { SEARCH_INPUT_PLACEHOLDER } from "app/Scenes/Search/Search"
import { goBack } from "app/system/navigation/navigate"

export const SearchModalScreen = () => {
return (
<Screen>
<Flex px={2} pt={2}>
<RoundSearchInput
placeholder={SEARCH_INPUT_PLACEHOLDER}
accessibilityHint="Search artists, artworks, galleries etc."
accessibilityLabel="Search artists, artworks, galleries etc."
maxLength={55}
numberOfLines={1}
autoFocus
multiline={false}
onLeftIconPress={() => {
goBack()
}}
/>
</Flex>
<Spacer y={2} />

<Flex flex={1} backgroundColor="black10" />
</Screen>
)
}
1 change: 1 addition & 0 deletions src/app/routes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -323,6 +323,7 @@ export function getDomainMap(): Record<string, RouteMatcher[] | null> {
addRoute("/recently-viewed", "RecentlyViewed"),
addRoute("/sell", "Sell"),
addRoute("/search", "Search"),
addRoute("/search/modal", "SearchModal"),
addRoute("/sell/inquiry", "ConsignmentInquiry"),
addRoute("/sell/submissions/new", "SubmitArtwork"),
addRoute("/sell/submissions/:externalID/edit", "SubmitArtworkEdit"),
Expand Down
5 changes: 5 additions & 0 deletions src/app/store/config/features.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down