diff --git a/src/CONST.ts b/src/CONST.ts index caca91a59055..2d04b386ce1c 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1108,7 +1108,7 @@ const CONST = { }, TIMING: { CALCULATE_MOST_RECENT_LAST_MODIFIED_ACTION: 'calc_most_recent_last_modified_action', - CHAT_FINDER_RENDER: 'search_render', + SEARCH_ROUTER_RENDER: 'search_router_render', CHAT_RENDER: 'chat_render', OPEN_REPORT: 'open_report', HOMEPAGE_INITIAL_RENDER: 'homepage_initial_render', diff --git a/src/ROUTES.ts b/src/ROUTES.ts index c2c404d7fea5..49572c14bdea 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -74,7 +74,6 @@ const ROUTES = { route: 'flag/:reportID/:reportActionID', getRoute: (reportID: string, reportActionID: string, backTo?: string) => getUrlWithBackToParam(`flag/${reportID}/${reportActionID}` as const, backTo), }, - CHAT_FINDER: 'chat-finder', PROFILE: { route: 'a/:accountID', getRoute: (accountID?: string | number, backTo?: string, login?: string) => { diff --git a/src/SCREENS.ts b/src/SCREENS.ts index 3da0f86d651e..d246eec31f66 100644 --- a/src/SCREENS.ts +++ b/src/SCREENS.ts @@ -143,7 +143,6 @@ const SCREENS = { ROOT: 'SaveTheWorld_Root', }, LEFT_MODAL: { - CHAT_FINDER: 'ChatFinder', WORKSPACE_SWITCHER: 'WorkspaceSwitcher', }, RIGHT_MODAL: { diff --git a/src/components/HeaderWithBackButton/index.tsx b/src/components/HeaderWithBackButton/index.tsx index eb04ad5540eb..e1843ee506d5 100755 --- a/src/components/HeaderWithBackButton/index.tsx +++ b/src/components/HeaderWithBackButton/index.tsx @@ -191,7 +191,7 @@ function HeaderWithBackButton({ /> )} {middleContent} - + {children} {shouldShowDownloadButton && ( diff --git a/src/components/Search/SearchRouter/SearchButton.tsx b/src/components/Search/SearchRouter/SearchButton.tsx index 05693ad5ea22..7ed22ec8162f 100644 --- a/src/components/Search/SearchRouter/SearchButton.tsx +++ b/src/components/Search/SearchRouter/SearchButton.tsx @@ -1,30 +1,37 @@ import React from 'react'; +import type {StyleProp, ViewStyle} from 'react-native'; import Icon from '@components/Icon'; import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; import useLocalize from '@hooks/useLocalize'; import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; -import Permissions from '@libs/Permissions'; +import Performance from '@libs/Performance'; +import * as Session from '@userActions/Session'; +import Timing from '@userActions/Timing'; +import CONST from '@src/CONST'; import {useSearchRouterContext} from './SearchRouterContext'; -function SearchButton() { +type SearchButtonProps = { + style?: StyleProp; +}; + +function SearchButton({style}: SearchButtonProps) { const styles = useThemeStyles(); const theme = useTheme(); const {translate} = useLocalize(); const {openSearchRouter} = useSearchRouterContext(); - if (!Permissions.canUseNewSearchRouter()) { - return; - } - return ( { + style={[styles.flexRow, styles.touchableButtonImage, style]} + onPress={Session.checkIfActionIsAllowed(() => { + Timing.start(CONST.TIMING.SEARCH_ROUTER_RENDER); + Performance.markStart(CONST.TIMING.SEARCH_ROUTER_RENDER); + openSearchRouter(); - }} + })} > void; +}; + +function SearchRouter({onRouterClose}: SearchRouterProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [betas] = useOnyx(ONYXKEYS.BETAS); @@ -37,7 +41,6 @@ function SearchRouter() { const [isSearchingForReports] = useOnyx(ONYXKEYS.IS_SEARCHING_FOR_REPORTS, {initWithStoredValues: false}); const {isSmallScreenWidth} = useResponsiveLayout(); - const {isSearchRouterDisplayed, closeSearchRouter} = useSearchRouterContext(); const listRef = useRef(null); const taxRates = getAllTaxRates(); @@ -69,7 +72,9 @@ function SearchRouter() { }; } + Timing.start(CONST.TIMING.SEARCH_FILTER_OPTIONS); const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedInputValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true}); + Timing.end(CONST.TIMING.SEARCH_FILTER_OPTIONS); return { recentReports: newOptions.recentReports, @@ -91,15 +96,6 @@ function SearchRouter() { Report.searchInServer(debouncedInputValue.trim()); }, [debouncedInputValue]); - useEffect(() => { - if (!textInputValue && isSearchRouterDisplayed) { - return; - } - listRef.current?.updateAndScrollToFocusedIndex(0); - // eslint-disable-next-line react-compiler/react-compiler - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [isSearchRouterDisplayed]); - const contextualReportData = contextualReportID ? searchOptions.recentReports?.find((option) => option.reportID === contextualReportID) : undefined; const clearUserQuery = () => { @@ -136,18 +132,18 @@ function SearchRouter() { }; const closeAndClearRouter = useCallback(() => { - closeSearchRouter(); + onRouterClose(); clearUserQuery(); // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - }, [closeSearchRouter]); + }, [onRouterClose]); const onSearchSubmit = useCallback( (query: SearchQueryJSON | undefined) => { if (!query) { return; } - closeSearchRouter(); + onRouterClose(); const standardizedQuery = SearchUtils.standardizeQueryJSON(query, cardList, taxRates); const queryString = SearchUtils.buildSearchQueryString(standardizedQuery); Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: queryString})); @@ -155,22 +151,24 @@ function SearchRouter() { }, // eslint-disable-next-line react-compiler/react-compiler // eslint-disable-next-line react-hooks/exhaustive-deps - [closeSearchRouter], + [onRouterClose], ); useKeyboardShortcut(CONST.KEYBOARD_SHORTCUTS.ESCAPE, () => { - closeSearchRouter(); - clearUserQuery(); + closeAndClearRouter(); }); - const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.popoverWidth}; + const modalWidth = isSmallScreenWidth ? styles.w100 : {width: variables.searchRouterPopoverWidth}; return ( - + {isSmallScreenWidth && ( closeSearchRouter()} + onBackButtonPress={() => onRouterClose()} /> )} { + onSearchSubmit(SearchUtils.buildSearchQueryJSON(textInputValue)); + }} routerListRef={listRef} - wrapperStyle={[isSmallScreenWidth ? styles.mv3 : styles.mv2, isSmallScreenWidth ? styles.mh5 : styles.mh2, styles.border]} + shouldShowOfflineMessage + wrapperStyle={[styles.border, styles.alignItemsCenter]} + outerWrapperStyle={[isSmallScreenWidth ? styles.mv3 : styles.mv2, isSmallScreenWidth ? styles.mh5 : styles.mh2]} wrapperFocusedStyle={[styles.borderColorFocus]} isSearchingForReports={isSearchingForReports} /> diff --git a/src/components/Search/SearchRouter/SearchRouterContext.tsx b/src/components/Search/SearchRouter/SearchRouterContext.tsx index d935fff110a4..2e4cbec0d6bb 100644 --- a/src/components/Search/SearchRouter/SearchRouterContext.tsx +++ b/src/components/Search/SearchRouter/SearchRouterContext.tsx @@ -1,10 +1,12 @@ -import React, {useContext, useMemo, useState} from 'react'; +import React, {useContext, useMemo, useRef, useState} from 'react'; +import * as Modal from '@userActions/Modal'; import type ChildrenProps from '@src/types/utils/ChildrenProps'; const defaultSearchContext = { isSearchRouterDisplayed: false, openSearchRouter: () => {}, closeSearchRouter: () => {}, + toggleSearchRouter: () => {}, }; type SearchRouterContext = typeof defaultSearchContext; @@ -13,15 +15,39 @@ const Context = React.createContext(defaultSearchContext); function SearchRouterContextProvider({children}: ChildrenProps) { const [isSearchRouterDisplayed, setIsSearchRouterDisplayed] = useState(false); + const searchRouterDisplayedRef = useRef(false); const routerContext = useMemo(() => { - const openSearchRouter = () => setIsSearchRouterDisplayed(true); - const closeSearchRouter = () => setIsSearchRouterDisplayed(false); + const openSearchRouter = () => { + Modal.close( + () => { + setIsSearchRouterDisplayed(true); + searchRouterDisplayedRef.current = true; + }, + false, + true, + ); + }; + const closeSearchRouter = () => { + setIsSearchRouterDisplayed(false); + searchRouterDisplayedRef.current = false; + }; + + // There are callbacks that live outside of React render-loop and interact with SearchRouter + // So we need a function that is based on ref to correctly open/close it + const toggleSearchRouter = () => { + if (searchRouterDisplayedRef.current) { + closeSearchRouter(); + } else { + openSearchRouter(); + } + }; return { isSearchRouterDisplayed, openSearchRouter, closeSearchRouter, + toggleSearchRouter, }; }, [isSearchRouterDisplayed, setIsSearchRouterDisplayed]); diff --git a/src/components/Search/SearchRouter/SearchRouterInput.tsx b/src/components/Search/SearchRouter/SearchRouterInput.tsx index 70e2e64074c9..ef6963152c42 100644 --- a/src/components/Search/SearchRouter/SearchRouterInput.tsx +++ b/src/components/Search/SearchRouter/SearchRouterInput.tsx @@ -2,9 +2,11 @@ import React, {useState} from 'react'; import type {ReactNode, RefObject} from 'react'; import {View} from 'react-native'; import type {StyleProp, ViewStyle} from 'react-native'; +import FormHelpMessage from '@components/FormHelpMessage'; import type {SelectionListHandle} from '@components/SelectionList/types'; import TextInput from '@components/TextInput'; import useLocalize from '@hooks/useLocalize'; +import useNetwork from '@hooks/useNetwork'; import useThemeStyles from '@hooks/useThemeStyles'; import variables from '@styles/variables'; import CONST from '@src/CONST'; @@ -31,6 +33,9 @@ type SearchRouterInputProps = { /** Whether the input is disabled */ disabled?: boolean; + /** Whether the offline message should be shown */ + shouldShowOfflineMessage?: boolean; + /** Whether the input should be focused */ autoFocus?: boolean; @@ -40,6 +45,9 @@ type SearchRouterInputProps = { /** Any additional styles to apply when input is focused */ wrapperFocusedStyle?: StyleProp; + /** Any additional styles to apply to text input along with FormHelperMessage */ + outerWrapperStyle?: StyleProp; + /** Component to be displayed on the right */ rightComponent?: ReactNode; @@ -55,15 +63,19 @@ function SearchRouterInput({ routerListRef, isFullWidth, disabled = false, + shouldShowOfflineMessage = false, autoFocus = true, wrapperStyle, wrapperFocusedStyle, + outerWrapperStyle, rightComponent, isSearchingForReports, }: SearchRouterInputProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const [isFocused, setIsFocused] = useState(false); + const {isOffline} = useNetwork(); + const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; const onChangeText = (text: string) => { setValue(text); @@ -73,34 +85,45 @@ function SearchRouterInput({ const inputWidth = isFullWidth ? styles.w100 : {width: variables.popoverWidth}; return ( - - - { - setIsFocused(true); - routerListRef?.current?.updateExternalTextInputFocus(true); - }} - onBlur={() => { - setIsFocused(false); - routerListRef?.current?.updateExternalTextInputFocus(false); - }} - isLoading={!!isSearchingForReports} - /> + + + + { + setIsFocused(true); + routerListRef?.current?.updateExternalTextInputFocus(true); + }} + onBlur={() => { + setIsFocused(false); + routerListRef?.current?.updateExternalTextInputFocus(false); + }} + isLoading={!!isSearchingForReports} + /> + + {rightComponent && {rightComponent}} - {rightComponent && {rightComponent}} + ); } diff --git a/src/components/Search/SearchRouter/SearchRouterList.tsx b/src/components/Search/SearchRouter/SearchRouterList.tsx index 20ade90843d7..9830ea4e9506 100644 --- a/src/components/Search/SearchRouter/SearchRouterList.tsx +++ b/src/components/Search/SearchRouter/SearchRouterList.tsx @@ -13,10 +13,13 @@ import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; +import Performance from '@libs/Performance'; import {getAllTaxRates} from '@libs/PolicyUtils'; import type {OptionData} from '@libs/ReportUtils'; import * as SearchUtils from '@libs/SearchUtils'; import * as Report from '@userActions/Report'; +import Timing from '@userActions/Timing'; +import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; @@ -47,6 +50,11 @@ type SearchRouterListProps = { closeAndClearRouter: () => void; }; +const setPerformanceTimersEnd = () => { + Timing.end(CONST.TIMING.SEARCH_ROUTER_RENDER); + Performance.markEnd(CONST.TIMING.SEARCH_ROUTER_RENDER); +}; + function isSearchQueryItem(item: OptionData | SearchQueryItem): item is SearchQueryItem { if ('singleIcon' in item && item.singleIcon && 'query' in item && item.query) { return true; @@ -72,7 +80,6 @@ function SearchRouterItem(props: UserListItemProps | SearchQueryList return ( @@ -136,7 +143,7 @@ function SearchRouterList( sections.push({title: translate('search.recentSearches'), data: recentSearchesData}); } - const styledRecentReports = recentReports.map((item) => ({...item, pressableStyle: styles.br2})); + const styledRecentReports = recentReports.map((item) => ({...item, pressableStyle: styles.br2, wrapperStyle: [styles.pr3, styles.pl3]})); sections.push({title: translate('search.recentChats'), data: styledRecentReports}); const onSelectRow = useCallback( @@ -158,7 +165,7 @@ function SearchRouterList( // Handle selection of "Recent chat" closeAndClearRouter(); if ('reportID' in item && item?.reportID) { - Navigation.closeAndNavigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID)); + Navigation.navigate(ROUTES.REPORT_WITH_ID.getRoute(item?.reportID)); } else if ('login' in item) { Report.navigateToAndOpenReport(item?.login ? [item.login] : []); } @@ -173,8 +180,12 @@ function SearchRouterList( ListItem={SearchRouterItem} containerStyle={[styles.mh100]} sectionListStyle={[isSmallScreenWidth ? styles.ph5 : styles.ph2, styles.pb2]} + listItemWrapperStyle={[styles.pr3, styles.pl3]} + onLayout={setPerformanceTimersEnd} ref={ref} showScrollIndicator={!isSmallScreenWidth} + sectionTitleStyles={styles.mhn2} + shouldSingleExecuteRowSelect /> ); } diff --git a/src/components/Search/SearchRouter/SearchRouterModal.tsx b/src/components/Search/SearchRouter/SearchRouterModal.tsx index 1f438d254a5f..7e403461dd34 100644 --- a/src/components/Search/SearchRouter/SearchRouterModal.tsx +++ b/src/components/Search/SearchRouter/SearchRouterModal.tsx @@ -17,10 +17,10 @@ function SearchRouterModal() { type={modalType} fullscreen isVisible={isSearchRouterDisplayed} - popoverAnchorPosition={{right: 20, top: 20}} + popoverAnchorPosition={{right: 6, top: 6}} onClose={closeSearchRouter} > - {isSearchRouterDisplayed && } + {isSearchRouterDisplayed && } ); } diff --git a/src/libs/E2E/reactNativeLaunchingTest.ts b/src/libs/E2E/reactNativeLaunchingTest.ts index f952998f0aad..fdd305baf88c 100644 --- a/src/libs/E2E/reactNativeLaunchingTest.ts +++ b/src/libs/E2E/reactNativeLaunchingTest.ts @@ -32,7 +32,7 @@ if (!appInstanceId) { // import your test here, define its name and config first in e2e/config.js const tests: Tests = { [E2EConfig.TEST_NAMES.AppStartTime]: require('./tests/appStartTimeTest.e2e').default, - [E2EConfig.TEST_NAMES.OpenChatFinderPage]: require('./tests/openChatFinderPageTest.e2e').default, + [E2EConfig.TEST_NAMES.OpenSearchRouter]: require('./tests/openSearchRouterTest.e2e').default, [E2EConfig.TEST_NAMES.ChatOpening]: require('./tests/chatOpeningTest.e2e').default, [E2EConfig.TEST_NAMES.ReportTyping]: require('./tests/reportTypingTest.e2e').default, [E2EConfig.TEST_NAMES.Linking]: require('./tests/linkingTest.e2e').default, diff --git a/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts similarity index 72% rename from src/libs/E2E/tests/openChatFinderPageTest.e2e.ts rename to src/libs/E2E/tests/openSearchRouterTest.e2e.ts index 2c2f2eda4efe..840af5acc2c9 100644 --- a/src/libs/E2E/tests/openChatFinderPageTest.e2e.ts +++ b/src/libs/E2E/tests/openSearchRouterTest.e2e.ts @@ -3,14 +3,12 @@ import E2ELogin from '@libs/E2E/actions/e2eLogin'; import waitForAppLoaded from '@libs/E2E/actions/waitForAppLoaded'; import E2EClient from '@libs/E2E/client'; import getPromiseWithResolve from '@libs/E2E/utils/getPromiseWithResolve'; -import Navigation from '@libs/Navigation/Navigation'; import Performance from '@libs/Performance'; import CONST from '@src/CONST'; -import ROUTES from '@src/ROUTES'; const test = () => { // check for login (if already logged in the action will simply resolve) - console.debug('[E2E] Logging in for chat finder'); + console.debug('[E2E] Logging in for new search router'); E2ELogin().then((neededLogin: boolean): Promise | undefined => { if (neededLogin) { @@ -20,36 +18,29 @@ const test = () => { ); } - console.debug('[E2E] Logged in, getting chat finder metrics and submitting them…'); + console.debug('[E2E] Logged in, getting search router metrics and submitting them…'); - const [openSearchPagePromise, openSearchPageResolve] = getPromiseWithResolve(); + const [openSearchRouterPromise, openSearchRouterResolve] = getPromiseWithResolve(); const [loadSearchOptionsPromise, loadSearchOptionsResolve] = getPromiseWithResolve(); - Promise.all([openSearchPagePromise, loadSearchOptionsPromise]).then(() => { + Promise.all([openSearchRouterPromise, loadSearchOptionsPromise]).then(() => { console.debug(`[E2E] Submitting!`); E2EClient.submitTestDone(); }); Performance.subscribeToMeasurements((entry) => { - if (entry.name === CONST.TIMING.SIDEBAR_LOADED) { - console.debug(`[E2E] Sidebar loaded, navigating to chat finder route…`); - Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); - Navigation.navigate(ROUTES.CHAT_FINDER); - return; - } - console.debug(`[E2E] Entry: ${JSON.stringify(entry)}`); - if (entry.name === CONST.TIMING.CHAT_FINDER_RENDER) { + if (entry.name === CONST.TIMING.SEARCH_ROUTER_RENDER) { E2EClient.submitTestResults({ branch: Config.E2E_BRANCH, - name: 'Open Chat Finder Page TTI', + name: 'Open Search Router TTI', metric: entry.duration, unit: 'ms', }) .then(() => { - openSearchPageResolve(); + openSearchRouterResolve(); console.debug('[E2E] Done with search, exiting…'); }) .catch((err) => { diff --git a/src/libs/Navigation/AppNavigator/AuthScreens.tsx b/src/libs/Navigation/AppNavigator/AuthScreens.tsx index f103504cbd86..7b8589c81e7f 100644 --- a/src/libs/Navigation/AppNavigator/AuthScreens.tsx +++ b/src/libs/Navigation/AppNavigator/AuthScreens.tsx @@ -7,6 +7,7 @@ import ActiveGuidesEventListener from '@components/ActiveGuidesEventListener'; import ComposeProviders from '@components/ComposeProviders'; import OptionsListContextProvider from '@components/OptionListContextProvider'; import {SearchContextProvider} from '@components/Search/SearchContext'; +import {useSearchRouterContext} from '@components/Search/SearchRouter/SearchRouterContext'; import SearchRouterModal from '@components/Search/SearchRouter/SearchRouterModal'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useOnboardingFlowRouter from '@hooks/useOnboardingFlow'; @@ -228,6 +229,8 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const screenOptions = getRootNavigatorScreenOptions(shouldUseNarrowLayout, styles, StyleUtils); const {canUseDefaultRooms} = usePermissions(); const {activeWorkspaceID} = useActiveWorkspace(); + const {toggleSearchRouter} = useSearchRouterContext(); + const onboardingModalScreenOptions = useMemo(() => screenOptions.onboardingModalNavigator(onboardingIsMediumOrLargerScreenWidth), [screenOptions, onboardingIsMediumOrLargerScreenWidth]); const onboardingScreenOptions = useMemo( () => getOnboardingModalScreenOptions(shouldUseNarrowLayout, styles, StyleUtils, onboardingIsMediumOrLargerScreenWidth), @@ -236,6 +239,7 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie const modal = useRef({}); const [didPusherInit, setDidPusherInit] = useState(false); const {isOnboardingCompleted} = useOnboardingFlowRouter(); + let initialReportID: string | undefined; const isInitialRender = useRef(true); if (isInitialRender.current) { @@ -346,16 +350,14 @@ function AuthScreens({session, lastOpenedPublicRoomID, initialLastUpdateIDApplie ); // Listen for the key K being pressed so that focus can be given to - // the chat switcher, or new group chat + // Search Router, or new group chat // based on the key modifiers pressed and the operating system const unsubscribeSearchShortcut = KeyboardShortcut.subscribe( searchShortcutConfig.shortcutKey, () => { - Modal.close( - Session.checkIfActionIsAllowed(() => Navigation.navigate(ROUTES.CHAT_FINDER)), - true, - true, - ); + Session.checkIfActionIsAllowed(() => { + toggleSearchRouter(); + })(); }, shortcutsOverviewShortcutConfig.descriptionKey, shortcutsOverviewShortcutConfig.modifiers, diff --git a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx index 077bdce94545..50439c19845e 100644 --- a/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx +++ b/src/libs/Navigation/AppNavigator/Navigators/LeftModalNavigator.tsx @@ -14,7 +14,6 @@ import Overlay from './Overlay'; type LeftModalNavigatorProps = StackScreenProps; -const loadChatFinder = () => require('../../../../pages/ChatFinderPage').default; const loadWorkspaceSwitcherPage = () => require('../../../../pages/WorkspaceSwitcherPage').default; const Stack = createStackNavigator(); @@ -37,10 +36,6 @@ function LeftModalNavigator({navigation}: LeftModalNavigatorProps) { screenOptions={screenOptions} id={NAVIGATORS.LEFT_MODAL_NAVIGATOR} > - sessionValue && {authTokenType: sessionValue.authTokenType}}); @@ -63,7 +56,7 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, {displaySignIn && } - {isCustomSearchQuery && ( + {shouldDisplayCancelSearch && ( {translate('common.cancel')} )} - {shouldDisplaySearchRouter && } - {displaySearch && ( - - { - Timing.start(CONST.TIMING.CHAT_FINDER_RENDER); - Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); - Navigation.navigate(ROUTES.CHAT_FINDER); - })} - > - - - - )} + {displaySearch && } ); diff --git a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx index 4684eb9637be..8967486165f8 100644 --- a/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx +++ b/src/libs/Navigation/AppNavigator/createCustomPlatformStackBottomTabNavigator/TopBar.tsx @@ -2,32 +2,25 @@ import React from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import Breadcrumbs from '@components/Breadcrumbs'; -import Icon from '@components/Icon'; -import * as Expensicons from '@components/Icon/Expensicons'; import {PressableWithoutFeedback} from '@components/Pressable'; import SearchButton from '@components/Search/SearchRouter/SearchButton'; import Text from '@components/Text'; -import Tooltip from '@components/Tooltip'; import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; -import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import Navigation from '@libs/Navigation/Navigation'; -import Performance from '@libs/Performance'; import * as SearchUtils from '@libs/SearchUtils'; import SignInButton from '@pages/home/sidebar/SignInButton'; import * as Session from '@userActions/Session'; -import Timing from '@userActions/Timing'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; isCustomSearchQuery?: boolean; shouldDisplaySearchRouter?: boolean}; +type TopBarProps = {breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; shouldDisplayCancelSearch?: boolean}; -function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, isCustomSearchQuery = false, shouldDisplaySearchRouter = false}: TopBarProps) { +function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false}: TopBarProps) { const styles = useThemeStyles(); - const theme = useTheme(); const {translate} = useLocalize(); const policy = usePolicy(activeWorkspaceID); const [session] = useOnyx(ONYXKEYS.SESSION, {selector: (sessionValue) => sessionValue && {authTokenType: sessionValue.authTokenType}}); @@ -63,7 +56,7 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, {displaySignIn && } - {isCustomSearchQuery && ( + {shouldDisplayCancelSearch && ( {translate('common.cancel')} )} - {shouldDisplaySearchRouter && } - {displaySearch && ( - - { - Timing.start(CONST.TIMING.CHAT_FINDER_RENDER); - Performance.markStart(CONST.TIMING.CHAT_FINDER_RENDER); - Navigation.navigate(ROUTES.CHAT_FINDER); - })} - > - - - - )} + {displaySearch && } ); diff --git a/src/libs/Navigation/linkingConfig/config.ts b/src/libs/Navigation/linkingConfig/config.ts index 8f152e2d35de..83b843df62aa 100644 --- a/src/libs/Navigation/linkingConfig/config.ts +++ b/src/libs/Navigation/linkingConfig/config.ts @@ -76,7 +76,6 @@ const config: LinkingOptions['config'] = { [SCREENS.NOT_FOUND]: '*', [NAVIGATORS.LEFT_MODAL_NAVIGATOR]: { screens: { - [SCREENS.LEFT_MODAL.CHAT_FINDER]: ROUTES.CHAT_FINDER, [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: { path: ROUTES.WORKSPACE_SWITCHER, }, diff --git a/src/libs/Navigation/types.ts b/src/libs/Navigation/types.ts index cbd6a2af95bf..6bd862c82c6c 100644 --- a/src/libs/Navigation/types.ts +++ b/src/libs/Navigation/types.ts @@ -1280,7 +1280,6 @@ type TransactionDuplicateNavigatorParamList = { }; type LeftModalNavigatorParamList = { - [SCREENS.LEFT_MODAL.CHAT_FINDER]: undefined; [SCREENS.LEFT_MODAL.WORKSPACE_SWITCHER]: undefined; }; diff --git a/src/libs/Permissions.ts b/src/libs/Permissions.ts index de3afbabadc2..24de2e612208 100644 --- a/src/libs/Permissions.ts +++ b/src/libs/Permissions.ts @@ -2,7 +2,6 @@ import type {OnyxEntry} from 'react-native-onyx'; import CONST from '@src/CONST'; import type {IOUType} from '@src/CONST'; import type Beta from '@src/types/onyx/Beta'; -import * as Environment from './Environment/Environment'; function canUseAllBetas(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.ALL); @@ -58,17 +57,6 @@ function canUseNewDotQBD(betas: OnyxEntry): boolean { return !!betas?.includes(CONST.BETAS.NEW_DOT_QBD) || canUseAllBetas(betas); } -/** - * New Search Router is under construction and for now should be displayed only in dev to allow developers to work on it. - * We are not using BETA for this feature, as betas are heavier to cleanup, - * and the development of new router is expected to take 2-3 weeks at most - * - * After everything is implemented this function can be removed, as we will always use SearchRouter in the App. - */ -function canUseNewSearchRouter() { - return Environment.isDevelopment(); -} - /** * Link previews are temporarily disabled. */ @@ -88,7 +76,6 @@ export default { canUseNewDotCopilot, canUseWorkspaceRules, canUseCombinedTrackSubmit, - canUseNewSearchRouter, canUseCategoryAndTagApprovers, canUseNewDotQBD, }; diff --git a/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx b/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx deleted file mode 100644 index 4c006abacfc7..000000000000 --- a/src/pages/ChatFinderPage/ChatFinderPageFooter.tsx +++ /dev/null @@ -1,11 +0,0 @@ -import React from 'react'; -import ReferralProgramCTA from '@components/ReferralProgramCTA'; -import CONST from '@src/CONST'; - -function ChatFinderPageFooter() { - return ; -} - -ChatFinderPageFooter.displayName = 'ChatFinderPageFooter'; - -export default ChatFinderPageFooter; diff --git a/src/pages/ChatFinderPage/index.tsx b/src/pages/ChatFinderPage/index.tsx deleted file mode 100644 index aabf881a8bed..000000000000 --- a/src/pages/ChatFinderPage/index.tsx +++ /dev/null @@ -1,209 +0,0 @@ -import type {StackScreenProps} from '@react-navigation/stack'; -import isEmpty from 'lodash/isEmpty'; -import React, {useCallback, useEffect, useMemo, useState} from 'react'; -import type {OnyxEntry} from 'react-native-onyx'; -import {withOnyx} from 'react-native-onyx'; -import HeaderWithBackButton from '@components/HeaderWithBackButton'; -import {useOptionsList} from '@components/OptionListContextProvider'; -import ScreenWrapper from '@components/ScreenWrapper'; -import SelectionList from '@components/SelectionList'; -import UserListItem from '@components/SelectionList/UserListItem'; -import useCancelSearchOnModalClose from '@hooks/useCancelSearchOnModalClose'; -import useDebouncedState from '@hooks/useDebouncedState'; -import useDismissedReferralBanners from '@hooks/useDismissedReferralBanners'; -import useLocalize from '@hooks/useLocalize'; -import useNetwork from '@hooks/useNetwork'; -import Navigation from '@libs/Navigation/Navigation'; -import type {RootStackParamList} from '@libs/Navigation/types'; -import * as OptionsListUtils from '@libs/OptionsListUtils'; -import Performance from '@libs/Performance'; -import type {OptionData} from '@libs/ReportUtils'; -import * as Report from '@userActions/Report'; -import Timing from '@userActions/Timing'; -import CONST from '@src/CONST'; -import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; -import type SCREENS from '@src/SCREENS'; -import type * as OnyxTypes from '@src/types/onyx'; -import ChatFinderPageFooter from './ChatFinderPageFooter'; - -type ChatFinderPageOnyxProps = { - /** Beta features list */ - betas: OnyxEntry; - - /** Whether or not we are searching for reports on the server */ - isSearchingForReports: OnyxEntry; -}; - -type ChatFinderPageProps = ChatFinderPageOnyxProps & StackScreenProps; - -type ChatFinderPageSectionItem = { - data: OptionData[]; - shouldShow: boolean; -}; - -type ChatFinderPageSectionList = ChatFinderPageSectionItem[]; - -const setPerformanceTimersEnd = () => { - Timing.end(CONST.TIMING.CHAT_FINDER_RENDER); - Performance.markEnd(CONST.TIMING.CHAT_FINDER_RENDER); -}; - -const ChatFinderPageFooterInstance = ; - -function ChatFinderPage({betas, isSearchingForReports, navigation}: ChatFinderPageProps) { - const [isScreenTransitionEnd, setIsScreenTransitionEnd] = useState(false); - const {translate} = useLocalize(); - const {isOffline} = useNetwork(); - const {options, areOptionsInitialized} = useOptionsList({ - shouldInitialize: isScreenTransitionEnd, - }); - - const offlineMessage: string = isOffline ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; - - const [searchValue, debouncedSearchValue, setSearchValue] = useDebouncedState(''); - const [, debouncedSearchValueInServer, setSearchValueInServer] = useDebouncedState('', 500); - const updateSearchValue = useCallback( - (value: string) => { - setSearchValue(value); - setSearchValueInServer(value); - }, - [setSearchValue, setSearchValueInServer], - ); - useCancelSearchOnModalClose(); - - useEffect(() => { - Report.searchInServer(debouncedSearchValueInServer.trim()); - }, [debouncedSearchValueInServer]); - - const searchOptions = useMemo(() => { - if (!areOptionsInitialized || !isScreenTransitionEnd) { - return { - recentReports: [], - personalDetails: [], - userToInvite: null, - currentUserOption: null, - categoryOptions: [], - tagOptions: [], - taxRatesOptions: [], - headerMessage: '', - }; - } - const optionList = OptionsListUtils.getSearchOptions(options, '', betas ?? []); - const header = OptionsListUtils.getHeaderMessage(optionList.recentReports.length + optionList.personalDetails.length !== 0, !!optionList.userToInvite, ''); - return {...optionList, headerMessage: header}; - }, [areOptionsInitialized, betas, isScreenTransitionEnd, options]); - - const filteredOptions = useMemo(() => { - if (debouncedSearchValue.trim() === '') { - return { - recentReports: [], - personalDetails: [], - userToInvite: null, - headerMessage: '', - }; - } - - Timing.start(CONST.TIMING.SEARCH_FILTER_OPTIONS); - const newOptions = OptionsListUtils.filterOptions(searchOptions, debouncedSearchValue, {sortByReportTypeInSearch: true, preferChatroomsOverThreads: true}); - Timing.end(CONST.TIMING.SEARCH_FILTER_OPTIONS); - - const header = OptionsListUtils.getHeaderMessage(newOptions.recentReports.length + Number(!!newOptions.userToInvite) > 0, false, debouncedSearchValue); - return { - recentReports: newOptions.recentReports, - personalDetails: newOptions.personalDetails, - userToInvite: newOptions.userToInvite, - headerMessage: header, - }; - }, [debouncedSearchValue, searchOptions]); - - const {recentReports, personalDetails: localPersonalDetails, userToInvite, headerMessage} = debouncedSearchValue.trim() !== '' ? filteredOptions : searchOptions; - - const sections = useMemo((): ChatFinderPageSectionList => { - const newSections: ChatFinderPageSectionList = []; - - if (recentReports?.length > 0) { - newSections.push({ - data: recentReports, - shouldShow: true, - }); - } - - if (localPersonalDetails.length > 0) { - newSections.push({ - data: localPersonalDetails, - shouldShow: true, - }); - } - - if (!isEmpty(userToInvite)) { - newSections.push({ - data: [userToInvite], - shouldShow: true, - }); - } - - return newSections; - }, [localPersonalDetails, recentReports, userToInvite]); - - const selectReport = (option: OptionData) => { - if (!option) { - return; - } - - if (option.reportID) { - Navigation.closeAndNavigate(ROUTES.REPORT_WITH_ID.getRoute(option.reportID)); - } else { - Report.navigateToAndOpenReport(option.login ? [option.login] : []); - } - }; - - const handleScreenTransitionEnd = () => { - setIsScreenTransitionEnd(true); - }; - - const {isDismissed} = useDismissedReferralBanners({referralContentType: CONST.REFERRAL_PROGRAM.CONTENT_TYPES.REFER_FRIEND}); - - return ( - - - - sections={areOptionsInitialized ? sections : CONST.EMPTY_ARRAY} - ListItem={UserListItem} - textInputValue={searchValue} - textInputLabel={translate('selectionList.nameEmailOrPhoneNumber')} - textInputHint={offlineMessage} - onChangeText={updateSearchValue} - headerMessage={headerMessage} - onLayout={setPerformanceTimersEnd} - onSelectRow={selectReport} - shouldSingleExecuteRowSelect - showLoadingPlaceholder={!areOptionsInitialized || !isScreenTransitionEnd} - footerContent={!isDismissed && ChatFinderPageFooterInstance} - isLoadingNewOptions={!!isSearchingForReports} - shouldDelayFocus={false} - /> - - ); -} - -ChatFinderPage.displayName = 'ChatFinderPage'; - -export default withOnyx({ - betas: { - key: ONYXKEYS.BETAS, - }, - isSearchingForReports: { - key: ONYXKEYS.IS_SEARCHING_FOR_REPORTS, - initWithStoredValues: false, - }, -})(ChatFinderPage); diff --git a/src/pages/Search/SearchPageBottomTab.tsx b/src/pages/Search/SearchPageBottomTab.tsx index 5bda7cd0056e..f1f38d8b3701 100644 --- a/src/pages/Search/SearchPageBottomTab.tsx +++ b/src/pages/Search/SearchPageBottomTab.tsx @@ -86,6 +86,8 @@ function SearchPageBottomTab() { ); } + const shouldDisplayCancelSearch = shouldUseNarrowLayout && !SearchUtils.isCannedSearchQuery(queryJSON); + return ( {shouldUseNarrowLayout ? ( diff --git a/src/pages/home/HeaderView.tsx b/src/pages/home/HeaderView.tsx index 42483cc3d223..31aaf7cb1f1c 100644 --- a/src/pages/home/HeaderView.tsx +++ b/src/pages/home/HeaderView.tsx @@ -280,7 +280,7 @@ function HeaderView({report, parentReportAction, reportID, onNavigationMenuButto {isTaskReport && !shouldUseNarrowLayout && ReportUtils.isOpenTaskReport(report, parentReportAction) && } {canJoin && !shouldUseNarrowLayout && joinButton} - + { @@ -43,6 +45,8 @@ function BaseSidebarScreen() { updateLastAccessedWorkspace(undefined); }, [activeWorkspace, activeWorkspaceID]); + const shouldDisplaySearch = shouldUseNarrowLayout; + return ( searchInputStyle: { color: theme.textSupporting, - fontSize: 13, - lineHeight: 16, + fontSize: variables.fontSizeNormal, + lineHeight: variables.fontSizeNormalHeight, }, searchRouterTextInputContainer: { diff --git a/src/styles/utils/spacing.ts b/src/styles/utils/spacing.ts index 22bc7cb9bbcb..fd0a3a3cabc7 100644 --- a/src/styles/utils/spacing.ts +++ b/src/styles/utils/spacing.ts @@ -55,6 +55,10 @@ export default { marginHorizontal: 32, }, + mhn2: { + marginHorizontal: -8, + }, + mhn5: { marginHorizontal: -20, }, diff --git a/src/styles/variables.ts b/src/styles/variables.ts index dccee6ed3e53..dc6655791489 100644 --- a/src/styles/variables.ts +++ b/src/styles/variables.ts @@ -167,6 +167,7 @@ export default { modalContentMaxWidth: 360, listItemHeightNormal: 64, popoverWidth: 375, + searchRouterPopoverWidth: 512, bankAccountActionPopoverRightSpacing: 32, bankAccountActionPopoverTopSpacing: 14, addPaymentPopoverRightSpacing: 23, diff --git a/tests/e2e/config.ts b/tests/e2e/config.ts index 4d4f1711a628..c8e89721c998 100644 --- a/tests/e2e/config.ts +++ b/tests/e2e/config.ts @@ -4,7 +4,7 @@ const OUTPUT_DIR = process.env.WORKING_DIRECTORY || './tests/e2e/results'; // add your test name here … const TEST_NAMES = { AppStartTime: 'App start time', - OpenChatFinderPage: 'Open chat finder page TTI', + OpenSearchRouter: 'Open search router TTI', ReportTyping: 'Report typing', ChatOpening: 'Chat opening', Linking: 'Linking', @@ -73,8 +73,8 @@ export default { name: TEST_NAMES.AppStartTime, // ... any additional config you might need }, - [TEST_NAMES.OpenChatFinderPage]: { - name: TEST_NAMES.OpenChatFinderPage, + [TEST_NAMES.OpenSearchRouter]: { + name: TEST_NAMES.OpenSearchRouter, }, [TEST_NAMES.ReportTyping]: { name: TEST_NAMES.ReportTyping, diff --git a/tests/perf-test/ChatFinderPage.perf-test.tsx b/tests/perf-test/SearchRouter.perf-test.tsx similarity index 66% rename from tests/perf-test/ChatFinderPage.perf-test.tsx rename to tests/perf-test/SearchRouter.perf-test.tsx index 4346977a1cd0..e9154a36a9a1 100644 --- a/tests/perf-test/ChatFinderPage.perf-test.tsx +++ b/tests/perf-test/SearchRouter.perf-test.tsx @@ -1,28 +1,23 @@ import type * as NativeNavigation from '@react-navigation/native'; -import type {StackNavigationProp, StackScreenProps} from '@react-navigation/stack'; import {fireEvent, screen} from '@testing-library/react-native'; import React, {useMemo} from 'react'; import type {ComponentType} from 'react'; import Onyx from 'react-native-onyx'; -import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; -import {measurePerformance} from 'reassure'; +import {measureRenders} from 'reassure'; import {LocaleContextProvider} from '@components/LocaleContextProvider'; import OptionListContextProvider, {OptionsListContext} from '@components/OptionListContextProvider'; +import SearchRouter from '@components/Search/SearchRouter/SearchRouter'; import {KeyboardStateProvider} from '@components/withKeyboardState'; import type {WithNavigationFocusProps} from '@components/withNavigationFocus'; -import type {RootStackParamList} from '@libs/Navigation/types'; import {createOptionList} from '@libs/OptionsListUtils'; -import ChatFinderPage from '@pages/ChatFinderPage'; import ComposeProviders from '@src/components/ComposeProviders'; import OnyxProvider from '@src/components/OnyxProvider'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type SCREENS from '@src/SCREENS'; -import type {Beta, PersonalDetails, Report} from '@src/types/onyx'; +import type {PersonalDetails, Report} from '@src/types/onyx'; import createCollection from '../utils/collections/createCollection'; import createPersonalDetails from '../utils/collections/personalDetails'; import createRandomReport from '../utils/collections/reports'; -import createAddListenerMock from '../utils/createAddListenerMock'; import * as TestHelper from '../utils/TestHelper'; import waitForBatchedUpdates from '../utils/waitForBatchedUpdates'; import wrapOnyxWithWaitForBatchedUpdates from '../utils/wrapOnyxWithWaitForBatchedUpdates'; @@ -68,6 +63,9 @@ jest.mock('@react-navigation/native', () => { getCurrentRoute: () => jest.fn(), getState: () => jest.fn(), }), + useNavigationState: () => ({ + routes: [], + }), }; }); @@ -86,15 +84,6 @@ jest.mock('@src/components/withNavigationFocus', () => (Component: ComponentType return WithNavigationFocus; }); -// mock of useDismissedReferralBanners -jest.mock('../../src/hooks/useDismissedReferralBanners', () => ({ - // eslint-disable-next-line @typescript-eslint/naming-convention - __esModule: true, - default: jest.fn(() => ({ - isDismissed: false, - setAsDismissed: () => {}, - })), -})); const getMockedReports = (length = 100) => createCollection( @@ -134,49 +123,33 @@ afterEach(() => { Onyx.clear(); }); -type ChatFinderPageProps = StackScreenProps & { - betas?: OnyxEntry; - reports?: OnyxCollection; - isSearchingForReports?: OnyxEntry; -}; +const mockOnClose = jest.fn(); -function ChatFinderPageWrapper(args: ChatFinderPageProps) { +function SearchRouterWrapper() { return ( - + ); } -function ChatFinderPageWithCachedOptions(args: ChatFinderPageProps) { +function SearchRouterWrapperWithCachedOptions() { return ( ({options: mockedOptions, initializeOptions: () => {}, areOptionsInitialized: true}), [])}> - + ); } -test('[ChatFinderPage] should render list with cached options', async () => { - const {addListener} = createAddListenerMock(); - +test('[SearchRouter] should render chat list with cached options', async () => { const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); + await screen.findByTestId('SearchRouter'); }; - const navigation = {addListener} as unknown as StackNavigationProp; - return waitForBatchedUpdates() .then(() => Onyx.multiSet({ @@ -186,31 +159,19 @@ test('[ChatFinderPage] should render list with cached options', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => - measurePerformance( - , - {scenario}, - ), - ); + .then(() => measureRenders(, {scenario})); }); -test('[ChatFinderPage] should interact when text input changes', async () => { - const {addListener} = createAddListenerMock(); - +test('[SearchRouter] should react to text input changes', async () => { const scenario = async () => { - await screen.findByTestId('ChatFinderPage'); + await screen.findByTestId('SearchRouter'); - const input = screen.getByTestId('selection-list-text-input'); + const input = screen.getByTestId('search-router-text-input'); fireEvent.changeText(input, 'Email Four'); fireEvent.changeText(input, 'Report'); fireEvent.changeText(input, 'Email Five'); }; - const navigation = {addListener} as unknown as StackNavigationProp; - return waitForBatchedUpdates() .then(() => Onyx.multiSet({ @@ -220,13 +181,5 @@ test('[ChatFinderPage] should interact when text input changes', async () => { [ONYXKEYS.IS_SEARCHING_FOR_REPORTS]: true, }), ) - .then(() => - measurePerformance( - , - {scenario}, - ), - ); + .then(() => measureRenders(, {scenario})); });