diff --git a/src/components/ExchangeAssetList.tsx b/src/components/ExchangeAssetList.tsx index c22c58e1ca3..086072e1a26 100644 --- a/src/components/ExchangeAssetList.tsx +++ b/src/components/ExchangeAssetList.tsx @@ -2,12 +2,11 @@ import { useIsFocused } from '@react-navigation/native'; import React, { forwardRef, ForwardRefRenderFunction, + MutableRefObject, ReactElement, useCallback, - useContext, useImperativeHandle, useMemo, - useRef, useState, } from 'react'; import { InteractionManager, Keyboard, SectionList, SectionListData } from 'react-native'; @@ -16,7 +15,6 @@ import { ButtonPressAnimation } from '@/components/animations'; import useAccountSettings from '@/hooks/useAccountSettings'; import FastCurrencySelectionRow from '@/components/asset-list/RecyclerAssetList2/FastComponents/FastCurrencySelectionRow'; import { ContactRow } from '@/components/contacts'; -import DiscoverSheetContext from '@/screens/discover/DiscoverScreenContext'; import { GradientText } from '@/components/text'; import { CopyToast, ToastPositionContainer } from '@/components/toasts'; import contextMenuProps from '@/components/exchangeAssetRowContextMenuProps'; @@ -125,10 +123,7 @@ const ExchangeAssetList: ForwardRefRenderFunction { - // eslint-disable-next-line react-hooks/rules-of-hooks - const { sectionListRef = useRef(null) } = useContext(DiscoverSheetContext) || { - sectionListRef: undefined, - }; + const sectionListRef = ref as MutableRefObject; useImperativeHandle(ref, () => sectionListRef.current as SectionList); const prevQuery = usePrevious(query); const { getParent: dangerouslyGetParent, navigate } = useNavigation(); diff --git a/src/components/ens-profile/ActionButtons/MoreButton.tsx b/src/components/ens-profile/ActionButtons/MoreButton.tsx index 1e662c71302..2d161ceddc5 100644 --- a/src/components/ens-profile/ActionButtons/MoreButton.tsx +++ b/src/components/ens-profile/ActionButtons/MoreButton.tsx @@ -29,9 +29,6 @@ export default function MoreButton({ address, ensName }: { address?: string; ens const { navigate } = useNavigation(); const { setClipboard } = useClipboard(); const { contacts, onRemoveContact } = useContacts(); - const { - params: { setIsSearchModeEnabled }, - } = useRoute(); const isSelectedWallet = useMemo(() => { const visibleWallet = selectedWallet.addresses?.find((wallet: { visible: boolean }) => wallet.visible); @@ -106,7 +103,6 @@ export default function MoreButton({ address, ensName }: { address?: string; ens async ({ nativeEvent: { actionKey } }) => { if (actionKey === ACTIONS.OPEN_WALLET) { if (!isSelectedWallet) { - setIsSearchModeEnabled?.(false); switchToWalletWithAddress(address!); } navigate(Routes.WALLET_SCREEN); @@ -140,17 +136,7 @@ export default function MoreButton({ address, ensName }: { address?: string; ens Share.share(android ? { message: shareLink } : { url: shareLink }); } }, - [ - address, - contact, - ensName, - isSelectedWallet, - navigate, - onRemoveContact, - setClipboard, - setIsSearchModeEnabled, - switchToWalletWithAddress, - ] + [address, contact, ensName, isSelectedWallet, navigate, onRemoveContact, setClipboard, switchToWalletWithAddress] ); const menuConfig = useMemo(() => ({ menuItems, ...(ios && { menuTitle: '' }) }), [menuItems]); diff --git a/src/components/native-context-menu/contextMenu.tsx b/src/components/native-context-menu/contextMenu.tsx index e2f304d0cf4..e53e43ac84c 100644 --- a/src/components/native-context-menu/contextMenu.tsx +++ b/src/components/native-context-menu/contextMenu.tsx @@ -20,7 +20,7 @@ import { // eslint-disable-next-line @typescript-eslint/ban-types type IconConfig = { iconType: 'ASSET' | 'SYSTEM' | (string & {}); iconValue: string; iconTint?: string | DynamicColor }; -type MenuActionConfig = Readonly< +export type MenuActionConfig = Readonly< { actionSubtitle?: string; // eslint-disable-next-line @typescript-eslint/ban-types diff --git a/src/components/showcase/ShowcaseHeader.js b/src/components/showcase/ShowcaseHeader.js index d49f02319fd..18bc4067fb6 100644 --- a/src/components/showcase/ShowcaseHeader.js +++ b/src/components/showcase/ShowcaseHeader.js @@ -135,9 +135,6 @@ export function Header() { const { handleSetSeedPhrase, handlePressImportButton } = useImportingWallet(); const onWatchAddress = useCallback(() => { - if (contextValue?.setIsSearchModeEnabled) { - contextValue.setIsSearchModeEnabled(false); - } handleSetSeedPhrase(contextValue.address); handlePressImportButton(color, contextValue.address, contextValue?.data?.profile?.accountSymbol); }, [contextValue, handleSetSeedPhrase, handlePressImportButton, color]); diff --git a/src/navigation/SwipeNavigator.tsx b/src/navigation/SwipeNavigator.tsx index eae5c76d1da..2219409ec97 100644 --- a/src/navigation/SwipeNavigator.tsx +++ b/src/navigation/SwipeNavigator.tsx @@ -7,7 +7,6 @@ import { TestnetToast } from '@/components/toasts'; import { DAPP_BROWSER, POINTS, useExperimentalFlag } from '@/config'; import { Box, Columns, globalColors, Stack, useForegroundColor, Text, Cover, useColorMode } from '@/design-system'; import { IS_ANDROID, IS_IOS, IS_TEST } from '@/env'; -import { isUsingButtonNavigation } from '@/utils/deviceUtils'; import { useAccountAccentColor, useAccountSettings, useCoinListEdited, useDimensions, usePendingTransactions } from '@/hooks'; import { useRemoteConfig } from '@/model/remoteConfig'; import RecyclerListViewScrollToTopProvider, { @@ -227,13 +226,11 @@ const TabBar = ({ descriptors, jumpTo, navigation, state }: TabBarProps) => { }, 5); } else if (isFocused && tabBarIcon === 'tabDiscover') { if (delta < DOUBLE_PRESS_DELAY) { - // @ts-expect-error No call signatures discoverOpenSearchFnRef?.(); return; } if (discoverScrollToTopFnRef?.() === 0) { - // @ts-expect-error No call signatures discoverOpenSearchFnRef?.(); return; } @@ -261,7 +258,6 @@ const TabBar = ({ descriptors, jumpTo, navigation, state }: TabBarProps) => { if (tabBarIcon === 'tabDiscover') { navigation.navigate(Routes.DISCOVER_SCREEN); InteractionManager.runAfterInteractions(() => { - // @ts-expect-error No call signatures discoverOpenSearchFnRef?.(); }); } diff --git a/src/resources/reservoir/mints.ts b/src/resources/reservoir/mints.ts index faa8375558a..7f28aaca8cc 100644 --- a/src/resources/reservoir/mints.ts +++ b/src/resources/reservoir/mints.ts @@ -17,7 +17,11 @@ const showAlert = () => { ); }; -export const navigateToMintCollection = async (contractAddress: EthereumAddress, pricePerMint: BigNumberish, chainId: ChainId) => { +export const navigateToMintCollection = async ( + contractAddress: EthereumAddress, + pricePerMint: BigNumberish | undefined, + chainId: ChainId +) => { logger.debug('[mints]: Navigating to Mint Collection', { contractAddress, chainId, diff --git a/src/screens/ShowcaseSheet.js b/src/screens/ShowcaseSheet.js index b1aa1791284..fc2f7e9f70e 100644 --- a/src/screens/ShowcaseSheet.js +++ b/src/screens/ShowcaseSheet.js @@ -42,7 +42,9 @@ const LoadingWrapper = styled.View({ }); export default function ShowcaseScreen() { - const { params: { address: addressOrDomain, setIsSearchModeEnabled } = {} } = useRoute(); + const { + params: { address: addressOrDomain }, + } = useRoute(); const theme = useTheme(); @@ -100,9 +102,8 @@ export default function ShowcaseScreen() { ...userData, address: accountAddress, addressOrDomain, - setIsSearchModeEnabled, }), - [userData, accountAddress, addressOrDomain, setIsSearchModeEnabled] + [userData, accountAddress, addressOrDomain] ); const loading = userData === null || isInitialLoading; diff --git a/src/screens/discover/DiscoverScreen.tsx b/src/screens/discover/DiscoverScreen.tsx index dd8ca846a58..601066d6260 100644 --- a/src/screens/discover/DiscoverScreen.tsx +++ b/src/screens/discover/DiscoverScreen.tsx @@ -1,11 +1,10 @@ -import React, { useCallback, useEffect } from 'react'; +import React, { useEffect } from 'react'; import { Keyboard } from 'react-native'; import { useIsFocused } from '@react-navigation/native'; import { Box } from '@/design-system'; import { Page } from '@/components/layout'; import { Navbar } from '@/components/navbar/Navbar'; import DiscoverScreenContent from './components/DiscoverScreenContent'; -import DiscoverSheetContext from './DiscoverScreenContext'; import { ButtonPressAnimation } from '@/components/animations'; import { ContactAvatar } from '@/components/contacts'; import ImageAvatar from '@/components/contacts/ImageAvatar'; @@ -15,42 +14,27 @@ import { useNavigation } from '@/navigation'; import { safeAreaInsetValues } from '@/utils'; import * as i18n from '@/languages'; import Animated, { useAnimatedScrollHandler, useSharedValue } from 'react-native-reanimated'; +import DiscoverScreenProvider, { useDiscoverScreenContext } from './DiscoverScreenContext'; export let discoverScrollToTopFnRef: () => number | null = () => null; -export default function DiscoverScreen() { - const ref = React.useRef(null); + +const Content = () => { const { navigate } = useNavigation(); const isFocused = useIsFocused(); const { accountSymbol, accountColor, accountImage } = useAccountProfile(); + const { isSearching, scrollToTop, scrollViewRef } = useDiscoverScreenContext(); const scrollY = useSharedValue(0); - const [isSearchModeEnabled, setIsSearchModeEnabled] = React.useState(false); const onChangeWallet = React.useCallback(() => { navigate(Routes.CHANGE_WALLET_SHEET); }, [navigate]); React.useEffect(() => { - if (isSearchModeEnabled && !isFocused) { + if (isSearching && !isFocused) { Keyboard.dismiss(); } - }, [isFocused, isSearchModeEnabled]); - - const scrollToTop = useCallback(() => { - if (!ref.current) return -1; - - // detect if scroll was already at top and return 0; - if (scrollY.value === 0) { - return 0; - } - - ref.current?.scrollTo({ - y: 0, - animated: true, - }); - - return 1; - }, [scrollY]); + }, [isFocused, isSearching]); const scrollHandler = useAnimatedScrollHandler({ onScroll: event => { @@ -60,43 +44,46 @@ export default function DiscoverScreen() { useEffect(() => { discoverScrollToTopFnRef = scrollToTop; - }, [ref, scrollToTop]); + }, [scrollToTop]); return ( - - - - {accountImage ? ( - - ) : ( - - )} - - } - testID={isSearchModeEnabled ? 'discover-header-search' : 'discover-header'} - title={isSearchModeEnabled ? i18n.t(i18n.l.discover.search.search) : i18n.t(i18n.l.discover.search.discover)} - /> - - - + + + {accountImage ? ( + + ) : ( + + )} + + } + testID={isSearching ? 'discover-header-search' : 'discover-header'} + title={isSearching ? i18n.t(i18n.l.discover.search.search) : i18n.t(i18n.l.discover.search.discover)} + /> + + - + + ); +}; + +export default function DiscoverScreen() { + return ( + + + ); } diff --git a/src/screens/discover/DiscoverScreenContext.js b/src/screens/discover/DiscoverScreenContext.js deleted file mode 100644 index 9491aedd621..00000000000 --- a/src/screens/discover/DiscoverScreenContext.js +++ /dev/null @@ -1,3 +0,0 @@ -import { createContext } from 'react'; - -export default createContext(null); diff --git a/src/screens/discover/DiscoverScreenContext.tsx b/src/screens/discover/DiscoverScreenContext.tsx new file mode 100644 index 00000000000..eb9b276443d --- /dev/null +++ b/src/screens/discover/DiscoverScreenContext.tsx @@ -0,0 +1,115 @@ +import { analytics } from '@/analytics'; +import React, { createContext, Dispatch, SetStateAction, RefObject, useState, useRef, useCallback } from 'react'; +import { SectionList, TextInput } from 'react-native'; +import Animated from 'react-native-reanimated'; + +type DiscoverScreenContextType = { + scrollViewRef: RefObject; + sectionListRef: RefObject; + searchInputRef: RefObject; + isSearching: boolean; + setIsSearching: Dispatch>; + isLoading: boolean; + setIsLoading: Dispatch>; + isFetchingEns: boolean; + setIsFetchingEns: Dispatch>; + searchQuery: string; + setSearchQuery: Dispatch>; + cancelSearch: () => void; + scrollToTop: () => number | null; + onTapSearch: () => void; +}; + +const DiscoverScreenContext = createContext(null); + +const sendQueryAnalytics = (query: string) => { + if (query.length > 1) { + analytics.track('Search Query', { + category: 'discover', + length: query.length, + query: query, + }); + } +}; + +const DiscoverScreenProvider = ({ children }: { children: React.ReactNode }) => { + const [isSearching, setIsSearching] = useState(false); + + const [isLoading, setIsLoading] = useState(false); + const [isFetchingEns, setIsFetchingEns] = useState(false); + const [searchQuery, setSearchQuery] = useState(''); + const searchInputRef = useRef(null); + + const scrollViewRef = useRef(null); + const sectionListRef = useRef(null); + + const scrollToTop = useCallback(() => { + if (isSearching) { + sectionListRef.current?.scrollToLocation({ + itemIndex: 0, + sectionIndex: 0, + animated: true, + }); + } else { + scrollViewRef.current?.scrollTo({ + y: 0, + animated: true, + }); + } + + return null; + }, [isSearching]); + + const onTapSearch = useCallback(() => { + if (isSearching) { + scrollToTop(); + searchInputRef.current?.focus(); + } else { + setIsSearching(true); + analytics.track('Tapped Search', { + category: 'discover', + }); + } + }, [isSearching, scrollToTop]); + + const cancelSearch = useCallback(() => { + searchInputRef.current?.blur(); + sendQueryAnalytics(searchQuery.trim()); + setIsLoading(false); + setSearchQuery(''); + setIsSearching(false); + }, [searchQuery]); + + return ( + + {children} + + ); +}; + +export default DiscoverScreenProvider; + +export const useDiscoverScreenContext = () => { + const context = React.useContext(DiscoverScreenContext); + if (!context) { + throw new Error('useDiscoverScreenContext must be used within a DiscoverScreenProvider'); + } + return context; +}; diff --git a/src/screens/discover/components/DiscoverScreenContent.js b/src/screens/discover/components/DiscoverScreenContent.tsx similarity index 51% rename from src/screens/discover/components/DiscoverScreenContent.js rename to src/screens/discover/components/DiscoverScreenContent.tsx index 2a8d88d05c3..1e3a7650013 100644 --- a/src/screens/discover/components/DiscoverScreenContent.js +++ b/src/screens/discover/components/DiscoverScreenContent.tsx @@ -1,32 +1,31 @@ -import React, { useRef, useState } from 'react'; +import React from 'react'; import { View } from 'react-native'; -import { FlexItem } from '@/components/layout'; +import { FlexItem, Page } from '@/components/layout'; import DiscoverHome from './DiscoverHome'; import DiscoverSearch from './DiscoverSearch'; import DiscoverSearchContainer from './DiscoverSearchContainer'; import { Box, Inset } from '@/design-system'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; +import { useDiscoverScreenContext } from '../DiscoverScreenContext'; -function Switcher({ showSearch, children }) { +function Switcher({ children }: { children: React.ReactNode[] }) { + const { isSearching } = useDiscoverScreenContext(); return ( <> - {showSearch ? children[0] : } - {children[1]} + {isSearching ? children[0] : } + {children[1]} ); } export default function DiscoverScreenContent() { - const [showSearch, setShowSearch] = useState(false); - const ref = useRef(); - const insets = useSafeAreaInsets(); return ( - + - - + + diff --git a/src/screens/discover/components/DiscoverSearch.js b/src/screens/discover/components/DiscoverSearch.tsx similarity index 69% rename from src/screens/discover/components/DiscoverSearch.js rename to src/screens/discover/components/DiscoverSearch.tsx index 7934398f5af..2e5880d7d35 100644 --- a/src/screens/discover/components/DiscoverSearch.js +++ b/src/screens/discover/components/DiscoverSearch.tsx @@ -1,60 +1,81 @@ -import lang from 'i18n-js'; -import { uniqBy } from 'lodash'; -import React, { useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react'; +import React, { useCallback, useEffect, useMemo, useState } from 'react'; import { InteractionManager, View } from 'react-native'; -import { IS_TESTING } from 'react-native-dotenv'; import { useDebounce } from 'use-debounce'; -import CurrencySelectionTypes from '@/helpers/currencySelectionTypes'; +import * as lang from '@/languages'; import deviceUtils from '@/utils/deviceUtils'; import CurrencySelectionList from '@/components/CurrencySelectionList'; import { Row } from '@/components/layout'; -import DiscoverSheetContext from '../DiscoverScreenContext'; +import { useDiscoverScreenContext } from '../DiscoverScreenContext'; import { analytics } from '@/analytics'; import { PROFILES, useExperimentalFlag } from '@/config'; -import { fetchSuggestions } from '@/handlers/ens'; -import { useAccountSettings, useHardwareBackOnFocus, usePrevious, useSearchCurrencyList } from '@/hooks'; +import { useAccountSettings, useSearchCurrencyList, usePrevious, useHardwareBackOnFocus } from '@/hooks'; import { useNavigation } from '@/navigation'; import Routes from '@/navigation/routesNames'; +import { fetchSuggestions } from '@/handlers/ens'; import styled from '@/styled-thing'; -import { useTheme } from '@/theme'; -import { ethereumUtils } from '@/utils'; +import { ethereumUtils, safeAreaInsetValues } from '@/utils'; import { getPoapAndOpenSheetWithQRHash, getPoapAndOpenSheetWithSecretWord } from '@/utils/poaps'; import { navigateToMintCollection } from '@/resources/reservoir/mints'; import { TAB_BAR_HEIGHT } from '@/navigation/SwipeNavigator'; import { ChainId, Network } from '@/chains/types'; import { chainsIdByName } from '@/chains'; +import { navbarHeight } from '@/components/navbar/Navbar'; +import { IS_TEST } from '@/env'; +import { uniqBy } from 'lodash'; +import { useTheme } from '@/theme'; +import { EnrichedExchangeAsset } from '@/components/ExchangeAssetList'; export const SearchContainer = styled(Row)({ height: '100%', }); +type EnsResult = { + address: string; + color: string; + ens: boolean; + image: string; + network: string; + nickname: string; + uniqueId: string; +}; + +type EnsSearchResult = { + color: string; + data: EnsResult[]; + key: string; + title: string; +}; + export default function DiscoverSearch() { const { navigate } = useNavigation(); const { accountAddress } = useAccountSettings(); + const { colors } = useTheme(); + const { isSearching, + isLoading, isFetchingEns, - setIsSearching, + setIsLoading, setIsFetchingEns, + cancelSearch, + setSearchQuery, searchQuery, - isSearchModeEnabled, - setIsSearchModeEnabled, searchInputRef, - cancelSearch, - } = useContext(DiscoverSheetContext); - - const { colors } = useTheme(); - const profilesEnabled = useExperimentalFlag(PROFILES); - const marginBottom = TAB_BAR_HEIGHT; + sectionListRef, + } = useDiscoverScreenContext(); - const currencySelectionListRef = useRef(); const [searchQueryForSearch] = useDebounce(searchQuery, 350); - const [ensResults, setEnsResults] = useState([]); + const [searchQueryForPoap] = useDebounce(searchQueryForSearch, 800); + + const lastSearchQuery = usePrevious(searchQueryForSearch); + + const [ensResults, setEnsResults] = useState([]); const { swapCurrencyList, swapCurrencyListLoading } = useSearchCurrencyList(searchQueryForSearch, ChainId.mainnet, true); - // we want to debounce the poap search further - const [searchQueryForPoap] = useDebounce(searchQueryForSearch, 800); + const profilesEnabled = useExperimentalFlag(PROFILES); + const marginBottom = TAB_BAR_HEIGHT + safeAreaInsetValues.bottom + 16; + const TOP_OFFSET = safeAreaInsetValues.top + navbarHeight; const currencyList = useMemo(() => { // order: @@ -79,8 +100,8 @@ export default function DiscoverSearch() { } // ONLY FOR e2e!!! Fake tokens with same symbols break detox e2e tests - if (IS_TESTING === 'true') { - let symbols = []; + if (IS_TEST) { + let symbols: string[] = []; list = list?.map(section => { // Remove dupes section.data = uniqBy(section?.data, 'symbol'); @@ -99,7 +120,6 @@ export default function DiscoverSearch() { } return list.filter(section => section.data.length > 0); }, [swapCurrencyList, ensResults]); - const lastSearchQuery = usePrevious(searchQueryForSearch); const currencyListDataKey = useMemo( () => `${swapCurrencyList?.[0]?.data?.[0]?.address || '_'}_${ensResults?.[0]?.data?.[0]?.address || '_'}`, @@ -113,7 +133,7 @@ export default function DiscoverSearch() { }); useEffect(() => { - const checkAndHandlePoaps = async secretWordOrHash => { + const checkAndHandlePoaps = async (secretWordOrHash: string) => { await getPoapAndOpenSheetWithSecretWord(secretWordOrHash); await getPoapAndOpenSheetWithQRHash(secretWordOrHash); }; @@ -123,25 +143,38 @@ export default function DiscoverSearch() { useEffect(() => { // probably dont need this entry point but seems worth keeping? // could do the same with zora, etc - const checkAndHandleMint = async seachQueryForMint => { + const checkAndHandleMint = async (seachQueryForMint: string) => { if (seachQueryForMint.includes('mint.fun')) { const mintdotfunURL = seachQueryForMint.split('https://mint.fun/'); const query = mintdotfunURL[1]; - let network = query.split('/')[0]; - if (network === 'ethereum') { - network = Network.mainnet; - } else if (network === 'op') { - network === Network.optimism; + const [networkName] = query.split('/'); + let chainId = chainsIdByName[networkName]; + if (!chainId) { + switch (networkName) { + case 'op': + chainId = ChainId.optimism; + break; + case 'ethereum': + chainId = ChainId.mainnet; + break; + case 'zora': + chainId = ChainId.zora; + break; + case 'base': + chainId = ChainId.base; + break; + } } const contractAddress = query.split('/')[1]; - navigateToMintCollection(contractAddress, chainsIdByName[network]); + navigateToMintCollection(contractAddress, undefined, chainId); + setSearchQuery(''); } }; checkAndHandleMint(searchQuery); - }, [accountAddress, navigate, searchQuery]); + }, [accountAddress, navigate, searchQuery, setSearchQuery]); const handlePress = useCallback( - item => { + (item: EnrichedExchangeAsset) => { if (item.ens) { // navigate to Showcase sheet searchInputRef?.current?.blur(); @@ -149,7 +182,6 @@ export default function DiscoverSearch() { navigate(profilesEnabled ? Routes.PROFILE_SHEET : Routes.SHOWCASE_SHEET, { address: item.nickname, fromRoute: 'DiscoverSearch', - setIsSearchModeEnabled, }); if (profilesEnabled) { analytics.track('Viewed ENS profile', { @@ -171,7 +203,7 @@ export default function DiscoverSearch() { }); } }, - [navigate, profilesEnabled, searchInputRef, setIsSearchModeEnabled] + [navigate, profilesEnabled, searchInputRef] ); const itemProps = useMemo( @@ -184,8 +216,8 @@ export default function DiscoverSearch() { ); const addEnsResults = useCallback( - ensResults => { - let ensSearchResults = []; + (ensResults: EnsResult[]) => { + let ensSearchResults: EnsSearchResult[] = []; if (ensResults && ensResults.length) { ensSearchResults = [ { @@ -202,45 +234,57 @@ export default function DiscoverSearch() { ); useEffect(() => { - if (searchQueryForSearch && !isSearching) { + if (searchQueryForSearch && !isLoading) { if (lastSearchQuery !== searchQueryForSearch) { - setIsSearching(true); + setIsLoading(true); fetchSuggestions(searchQuery, addEnsResults, setIsFetchingEns, profilesEnabled); } } - }, [addEnsResults, isSearching, lastSearchQuery, searchQuery, searchQueryForSearch, setIsFetchingEns, setIsSearching, profilesEnabled]); + }, [ + addEnsResults, + isSearching, + lastSearchQuery, + searchQuery, + setIsFetchingEns, + profilesEnabled, + isLoading, + setIsLoading, + searchQueryForSearch, + ]); useEffect(() => { if (!swapCurrencyListLoading && !isFetchingEns) { - setIsSearching(false); + setIsLoading(false); } - }, [isFetchingEns, setIsSearching, swapCurrencyListLoading]); + }, [isFetchingEns, setIsLoading, swapCurrencyListLoading]); useEffect(() => { - currencySelectionListRef.current?.scrollToLocation({ - animated: false, + if (!sectionListRef.current?.props.data?.length) { + return; + } + + sectionListRef.current.scrollToLocation({ itemIndex: 0, sectionIndex: 0, - viewOffset: 0, - viewPosition: 0, + animated: true, }); - }, [isSearchModeEnabled]); + }, [sectionListRef, isSearching]); return ( - + diff --git a/src/screens/discover/components/DiscoverSearchContainer.js b/src/screens/discover/components/DiscoverSearchContainer.js deleted file mode 100644 index a7cfc343021..00000000000 --- a/src/screens/discover/components/DiscoverSearchContainer.js +++ /dev/null @@ -1,149 +0,0 @@ -import { useRoute } from '@react-navigation/native'; -import lang from 'i18n-js'; -import React, { forwardRef, useCallback, useContext, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'; -import { ButtonPressAnimation } from '@/components/animations'; -import { Column, Row } from '@/components/layout'; -import { Text } from '@/components/text'; -import DiscoverSearchInput from '@/components/discover/DiscoverSearchInput'; -import DiscoverSheetContext from '../DiscoverScreenContext'; -import { deviceUtils } from '@/utils'; -import { analytics } from '@/analytics'; -import { useDelayedValueWithLayoutAnimation } from '@/hooks'; -import styled from '@/styled-thing'; - -const CancelButton = styled(ButtonPressAnimation)({ - marginTop: 9, -}); - -const CancelText = styled(Text).attrs(({ theme: { colors } }) => ({ - align: 'right', - color: colors.appleBlue, - letterSpacing: 'roundedMedium', - size: 'large', - weight: 'semibold', -}))({ - ...(ios ? {} : { marginTop: -5 }), - marginLeft: -3, - marginRight: 15, -}); - -const sendQueryAnalytics = query => { - if (query.length > 1) { - analytics.track('Search Query', { - category: 'discover', - length: query.length, - query: query, - }); - } -}; - -export let discoverOpenSearchFnRef = null; - -export default forwardRef(function DiscoverSearchContainer({ children, showSearch, setShowSearch }, ref) { - const searchInputRef = useRef(); - const sectionListRef = useRef(); - useImperativeHandle(ref, () => searchInputRef.current); - const [searchQuery, setSearchQuery] = useState(''); - const [isSearching, setIsSearching] = useState(false); - const [isFetchingEns, setIsFetchingEns] = useState(false); - const delayedShowSearch = useDelayedValueWithLayoutAnimation(showSearch); - - const { setIsSearchModeEnabled, isSearchModeEnabled } = useContext(DiscoverSheetContext); - - const setIsInputFocused = useCallback( - value => { - setShowSearch(value); - setIsSearchModeEnabled(value); - }, - [setIsSearchModeEnabled, setShowSearch] - ); - - const scrollToTop = useCallback(() => { - sectionListRef.current?.scrollToLocation({ - animated: true, - itemIndex: 0, - sectionIndex: 0, - viewOffset: 0, - viewPosition: 0, - }); - }, []); - - const onTapSearch = useCallback(() => { - if (isSearchModeEnabled) { - scrollToTop(); - searchInputRef.current.focus(); - } else { - setIsInputFocused(true); - analytics.track('Tapped Search', { - category: 'discover', - }); - } - }, [isSearchModeEnabled, scrollToTop, setIsInputFocused]); - - useEffect(() => { - discoverOpenSearchFnRef = onTapSearch; - }, [onTapSearch]); - - const cancelSearch = useCallback(() => { - searchInputRef.current?.blur(); - setIsInputFocused(false); - sendQueryAnalytics(searchQuery); - }, [searchInputRef, setIsInputFocused, searchQuery]); - - const contextValue = useMemo( - () => ({ - isSearchModeEnabled, - setIsSearchModeEnabled, - cancelSearch, - isFetchingEns, - isSearching, - searchInputRef, - searchQuery, - sectionListRef, - setIsFetchingEns, - setIsSearching, - }), - [isSearchModeEnabled, setIsSearchModeEnabled, cancelSearch, isFetchingEns, isSearching, searchQuery] - ); - - useEffect(() => { - if (!isSearchModeEnabled) { - setSearchQuery(''); - setIsSearching(false); - setIsFetchingEns(false); - searchInputRef.current?.blur(); - setIsInputFocused(false); - } else if (!searchInputRef.current.isFocused()) { - searchInputRef.current?.focus(); - } - }, [isSearchModeEnabled, setIsInputFocused]); - - const placeholderText = deviceUtils.isNarrowPhone - ? lang.t('discover.search.search_ethereum_short') - : lang.t('discover.search.search_ethereum'); - return ( - <> - - - setIsInputFocused(false)} - onChangeText={setSearchQuery} - onFocus={onTapSearch} - placeholderText={isSearchModeEnabled ? placeholderText : `􀊫 ${placeholderText}`} - ref={searchInputRef} - searchQuery={searchQuery} - testID="discover-search" - /> - - - {delayedShowSearch && {lang.t('button.done')}} - - - {children} - - ); -}); diff --git a/src/screens/discover/components/DiscoverSearchContainer.tsx b/src/screens/discover/components/DiscoverSearchContainer.tsx new file mode 100644 index 00000000000..fa7fabff669 --- /dev/null +++ b/src/screens/discover/components/DiscoverSearchContainer.tsx @@ -0,0 +1,71 @@ +import lang from 'i18n-js'; +import React, { useEffect } from 'react'; +import { ButtonPressAnimation } from '@/components/animations'; +import { Column, Row } from '@/components/layout'; +import { Text } from '@/components/text'; +import DiscoverSearchInput from '@/screens/discover/components/DiscoverSearchInput'; +import { useDiscoverScreenContext } from '../DiscoverScreenContext'; +import { deviceUtils } from '@/utils'; +import { useDelayedValueWithLayoutAnimation } from '@/hooks'; +import styled from '@/styled-thing'; +import { ThemeContextProps } from '@/theme'; + +const CancelButton = styled(ButtonPressAnimation)({ + marginTop: 9, +}); + +type WithThemeProps = { + theme: ThemeContextProps; +}; + +const CancelText = styled(Text).attrs(({ theme: { colors } }: WithThemeProps) => ({ + align: 'right', + color: colors.appleBlue, + letterSpacing: 'roundedMedium', + size: 'large', + weight: 'semibold', +}))({ + ...(ios ? {} : { marginTop: -5 }), + marginLeft: -3, + marginRight: 15, +}); + +const placeholderText = deviceUtils.isNarrowPhone + ? lang.t('discover.search.search_ethereum_short') + : lang.t('discover.search.search_ethereum'); + +export let discoverOpenSearchFnRef: () => void = () => null; + +function DiscoverSearchContainer({ children }: { children: React.ReactNode }) { + const { searchQuery, setSearchQuery, isLoading, isSearching, onTapSearch, cancelSearch } = useDiscoverScreenContext(); + const delayedShowSearch = useDelayedValueWithLayoutAnimation(isSearching); + + useEffect(() => { + discoverOpenSearchFnRef = onTapSearch; + }, [onTapSearch]); + + return ( + <> + + + + + + {delayedShowSearch && {lang.t('button.done')}} + + + {children} + + ); +} + +export default DiscoverSearchContainer; diff --git a/src/components/discover/DiscoverSearchInput.js b/src/screens/discover/components/DiscoverSearchInput.tsx similarity index 60% rename from src/components/discover/DiscoverSearchInput.js rename to src/screens/discover/components/DiscoverSearchInput.tsx index affbc4bf629..e03716fa877 100644 --- a/src/components/discover/DiscoverSearchInput.js +++ b/src/screens/discover/components/DiscoverSearchInput.tsx @@ -1,33 +1,42 @@ import lang from 'i18n-js'; import { isEmpty } from 'lodash'; -import React, { useCallback, useContext, useEffect, useMemo, useRef } from 'react'; +import React, { useCallback, useEffect, useMemo, useRef } from 'react'; import RadialGradient from 'react-native-radial-gradient'; -import Animated, { Easing, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'; -import Spinner from '../../assets/chartSpinner.png'; -import { ClearInputDecorator, Input } from '../inputs'; -import { Row } from '../layout'; -import { Text } from '../text'; +import Animated, { Easing, useAnimatedProps, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated'; +import Spinner from '@/assets/chartSpinner.png'; +import { ClearInputDecorator, Input } from '@/components/inputs'; +import { Row } from '@/components/layout'; +import { Text } from '@/components/text'; import { analytics } from '@/analytics'; import { ImgixImage } from '@/components/images'; import styled from '@/styled-thing'; import { margin, padding } from '@/styles'; import { deviceUtils } from '@/utils'; -import DiscoverSheetContext from '@/screens/discover/DiscoverScreenContext'; import { chainsName } from '@/chains'; +import { ThemeContextProps } from '@/theme'; +import { ChainId } from '@/chains/types'; +import { useDiscoverScreenContext } from '@/screens/discover/DiscoverScreenContext'; const SearchHeight = 40; const SearchWidth = deviceUtils.dimensions.width - 30; -const Container = styled(Row)(({ isSearchModeEnabled, theme: { colors } }) => ({ - ...margin.object(0, 15, isSearchModeEnabled ? 8 : 0), - ...(isSearchModeEnabled ? padding.object(0, 37, 0, 12) : padding.object(0)), +type ContainerProps = { + isSearching: boolean; + isDiscover: boolean; + clearTextOnFocus: boolean; + theme: ThemeContextProps; +}; + +const Container = styled(Row)(({ isSearching, theme: { colors } }: ContainerProps) => ({ + ...margin.object(0, 15, isSearching ? 8 : 0), + ...(isSearching ? padding.object(0, 37, 0, 12) : padding.object(0)), backgroundColor: colors.transparent, borderRadius: SearchHeight / 2, height: SearchHeight, overflow: 'hidden', })); -const BackgroundGradient = styled(RadialGradient).attrs(({ isDiscover, theme: { colors } }) => ({ +const BackgroundGradient = styled(RadialGradient).attrs(({ isDiscover, theme: { colors } }: ContainerProps) => ({ center: [SearchWidth, SearchWidth / 2], colors: isDiscover ? colors.gradients.searchBar : colors.gradients.lightGreyTransparent, }))({ @@ -38,7 +47,7 @@ const BackgroundGradient = styled(RadialGradient).attrs(({ isDiscover, theme: { width: SearchWidth, }); -const SearchIcon = styled(Text).attrs(({ theme: { colors } }) => ({ +const SearchIcon = styled(Text).attrs(({ theme: { colors } }: ContainerProps) => ({ color: colors.alpha(colors.blueGreyDark, 0.6), size: 'large', weight: 'semibold', @@ -48,7 +57,7 @@ const SearchIconWrapper = styled(Animated.View)({ marginTop: android ? 4 : 9, }); -const SearchInput = styled(Input).attrs(({ theme: { colors }, isSearchModeEnabled, clearTextOnFocus }) => ({ +const SearchInput = styled(Input).attrs(({ theme: { colors }, isSearching, clearTextOnFocus }: ContainerProps) => ({ blurOnSubmit: false, clearTextOnFocus, color: colors.alpha(colors.blueGreyDark, 0.8), @@ -58,7 +67,7 @@ const SearchInput = styled(Input).attrs(({ theme: { colors }, isSearchModeEnable lineHeight: 'looserLoose', placeholderTextColor: colors.alpha(colors.blueGreyDark, 0.6), returnKeyType: 'search', - selectionColor: isSearchModeEnabled ? colors.appleBlue : colors.transparent, + selectionColor: isSearching ? colors.appleBlue : colors.transparent, size: 'large', spellCheck: false, weight: 'semibold', @@ -67,11 +76,11 @@ const SearchInput = styled(Input).attrs(({ theme: { colors }, isSearchModeEnable flex: 1, height: ios ? 39 : 56, marginBottom: 1, - marginLeft: ({ isSearchModeEnabled }) => (isSearchModeEnabled ? 4 : 0), - textAlign: ({ isSearchModeEnabled }) => (isSearchModeEnabled ? 'left' : 'center'), + marginLeft: ({ isSearching }: ContainerProps) => (isSearching ? 4 : 0), + textAlign: ({ isSearching }: ContainerProps) => (isSearching ? 'left' : 'center'), }); -const SearchSpinner = styled(ImgixImage).attrs(({ theme: { colors } }) => ({ +const SearchSpinner = styled(ImgixImage).attrs(({ theme: { colors } }: ContainerProps) => ({ resizeMode: ImgixImage.resizeMode.contain, source: Spinner, tintColor: colors.alpha(colors.blueGreyDark, 0.6), @@ -98,21 +107,32 @@ const timingConfig = { duration: 300, }; -const DiscoverSearchInput = ( - { - isDiscover, - isFetching, - isSearching, - onChangeText, - onFocus, - searchQuery, - testID, - placeholderText = lang.t('button.exchange_search_placeholder'), - clearTextOnFocus = true, - currentChainId, - }, - ref -) => { +type DiscoverSearchInputProps = { + isDiscover: boolean; + isLoading?: boolean; + onChangeText: (text: string) => void; + onFocus: ({ target }: { target: any }) => void; + onBlur?: () => void; + searchQuery: string; + testID: string; + placeholderText?: string; + clearTextOnFocus: boolean; + currentChainId?: ChainId; +}; + +const DiscoverSearchInput = ({ + isDiscover, + isLoading = false, + onChangeText, + onFocus, + onBlur, + searchQuery, + testID, + placeholderText = lang.t('button.exchange_search_placeholder'), + clearTextOnFocus = true, + currentChainId, +}: DiscoverSearchInputProps) => { + const { searchInputRef, isSearching } = useDiscoverScreenContext(); const handleClearInput = useCallback(() => { if (isDiscover && searchQuery.length > 1) { analytics.track('Search Query', { @@ -121,13 +141,12 @@ const DiscoverSearchInput = ( query: searchQuery, }); } - ref?.current?.clear(); + searchInputRef?.current?.clear(); onChangeText?.(''); - }, [isDiscover, searchQuery, ref, onChangeText]); + }, [isDiscover, searchQuery, searchInputRef, onChangeText]); const spinnerRotation = useSharedValue(0); const spinnerScale = useSharedValue(0); - const { isSearchModeEnabled = true } = useContext(DiscoverSheetContext) || {}; const placeholder = useMemo(() => { if (!currentChainId) return placeholderText; @@ -136,10 +155,12 @@ const DiscoverSearchInput = ( }); }, [currentChainId, placeholderText]); - const spinnerTimeout = useRef(); + const spinnerTimeout = useRef(null); useEffect(() => { - if ((isFetching || isSearching) && !isEmpty(searchQuery)) { - clearTimeout(spinnerTimeout.current); + if (isLoading && !isEmpty(searchQuery)) { + if (spinnerTimeout.current) { + clearTimeout(spinnerTimeout.current); + } spinnerRotation.value = 0; spinnerRotation.value = withRepeat(withTiming(360, rotationConfig), -1, false); spinnerScale.value = withTiming(1, timingConfig); @@ -147,8 +168,7 @@ const DiscoverSearchInput = ( spinnerScale.value = withTiming(0, timingConfig); spinnerTimeout.current = setTimeout(() => (spinnerRotation.value = 0), timingConfig.duration); } - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isFetching, isSearching, searchQuery]); + }, [isLoading, searchQuery, spinnerRotation, spinnerScale]); const searchIconStyle = useAnimatedStyle(() => { return { @@ -164,10 +184,19 @@ const DiscoverSearchInput = ( }; }); + const searchInputValue = useAnimatedProps(() => { + // Removing the value when the input is focused allows the input to be reset to the correct value on blur + const query = isLoading ? undefined : ''; + return { + text: query, + defaultValue: '', + }; + }); + return ( - + - {isSearchModeEnabled && ( + {isSearching && ( <> 􀊫 @@ -178,14 +207,16 @@ const DiscoverSearchInput = ( )} { +export const getPoapAndOpenSheetWithSecretWord = async (secretWord: string, goBack = false) => { try { const event = await arcClient.getPoapEventBySecretWord({ secretWord, @@ -24,7 +24,7 @@ export const getPoapAndOpenSheetWithSecretWord = async (secretWord: string, goBa } }; -export const getPoapAndOpenSheetWithQRHash = async (qrHash: string, goBack: boolean) => { +export const getPoapAndOpenSheetWithQRHash = async (qrHash: string, goBack = false) => { try { const event = await arcClient.getPoapEventByQrHash({ qrHash,