diff --git a/patches/react-native-reanimated+3.16.4+004+fix-transform-animation-gitter.patch b/patches/react-native-reanimated+3.16.4+004+fix-transform-animation-gitter.patch new file mode 100644 index 000000000000..9e7657c2dda4 --- /dev/null +++ b/patches/react-native-reanimated+3.16.4+004+fix-transform-animation-gitter.patch @@ -0,0 +1,20 @@ +diff --git a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp +index 673ebd1..08d65fe 100644 +--- a/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp ++++ b/node_modules/react-native-reanimated/Common/cpp/reanimated/NativeModules/NativeReanimatedModule.cpp +@@ -710,6 +710,15 @@ void NativeReanimatedModule::performOperations() { + jsPropsUpdater.call(rt, viewTag, nonAnimatableProps); + } + ++ if (propsRegistry_->shouldReanimatedSkipCommit()) { ++ // It may happen that `performOperations` is called on the UI thread ++ // while React Native tries to commit a new tree on the JS thread. ++ // In this case, we should skip the commit here and let React Native do ++ // it. The commit will include the current values from PropsRegistry which ++ // will be applied in ReanimatedCommitHook. ++ return; ++ } ++ + bool hasLayoutUpdates = false; + for (const auto &[shadowNode, props] : copiedOperationsQueue) { + if (isThereAnyLayoutProp(rt, props->asObject(rt))) { diff --git a/src/ROUTES.ts b/src/ROUTES.ts index b98717b51f5d..b05985507176 100644 --- a/src/ROUTES.ts +++ b/src/ROUTES.ts @@ -37,7 +37,7 @@ const ROUTES = { // This route renders the list of reports. HOME: 'home', - SEARCH_CENTRAL_PANE: { + SEARCH_ROOT: { route: 'search', getRoute: ({query, name}: {query: SearchQueryString; name?: string}) => `search?q=${encodeURIComponent(query)}${name ? `&name=${name}` : ''}` as const, }, diff --git a/src/components/Navigation/BottomTabBar/index.tsx b/src/components/Navigation/BottomTabBar/index.tsx index f516eb43d04f..729f234c5b4b 100644 --- a/src/components/Navigation/BottomTabBar/index.tsx +++ b/src/components/Navigation/BottomTabBar/index.tsx @@ -116,7 +116,7 @@ function BottomTabBar({selectedTab, isTooltipAllowed = false}: BottomTabBarProps const cleanedQuery = handleQueryWithPolicyID(q, activeWorkspaceID); Navigation.navigate( - ROUTES.SEARCH_CENTRAL_PANE.getRoute({ + ROUTES.SEARCH_ROOT.getRoute({ query: cleanedQuery, ...rest, }), @@ -127,7 +127,7 @@ function BottomTabBar({selectedTab, isTooltipAllowed = false}: BottomTabBarProps const defaultCannedQuery = SearchQueryUtils.buildCannedSearchQuery(); // when navigating to search we might have an activePolicyID set from workspace switcher const query = activeWorkspaceID ? `${defaultCannedQuery} ${CONST.SEARCH.SYNTAX_ROOT_KEYS.POLICY_ID}:${activeWorkspaceID}` : defaultCannedQuery; - Navigation.navigate(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query})); + Navigation.navigate(ROUTES.SEARCH_ROOT.getRoute({query})); }); }, [activeWorkspaceID, selectedTab]); diff --git a/src/components/Navigation/TopBar.tsx b/src/components/Navigation/TopBar.tsx index 775e60666bef..a9ca480aed90 100644 --- a/src/components/Navigation/TopBar.tsx +++ b/src/components/Navigation/TopBar.tsx @@ -10,28 +10,25 @@ import WorkspaceSwitcherButton from '@components/WorkspaceSwitcherButton'; import useLocalize from '@hooks/useLocalize'; import usePolicy from '@hooks/usePolicy'; import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@libs/Navigation/Navigation'; -import * as SearchQueryUtils from '@libs/SearchQueryUtils'; import SignInButton from '@pages/home/sidebar/SignInButton'; -import * as Session from '@userActions/Session'; +import {isAnonymousUser as isAnonymousUserUtil} from '@userActions/Session'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import ROUTES from '@src/ROUTES'; type TopBarProps = { breadcrumbLabel: string; activeWorkspaceID?: string; shouldDisplaySearch?: boolean; - shouldDisplayCancelSearch?: boolean; + cancelSearch?: () => void; }; -function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, shouldDisplayCancelSearch = false}: TopBarProps) { +function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, cancelSearch}: TopBarProps) { const styles = useThemeStyles(); const {translate} = useLocalize(); const policy = usePolicy(activeWorkspaceID); const [session] = useOnyx(ONYXKEYS.SESSION, {selector: (sessionValue) => sessionValue && {authTokenType: sessionValue.authTokenType}}); const [isLoadingReportData] = useOnyx(ONYXKEYS.IS_LOADING_REPORT_DATA); - const isAnonymousUser = Session.isAnonymousUser(session); + const isAnonymousUser = isAnonymousUserUtil(session); const headerBreadcrumb = policy?.name ? {type: CONST.BREADCRUMB_TYPE.STRONG, text: policy.name} @@ -63,12 +60,12 @@ function TopBar({breadcrumbLabel, activeWorkspaceID, shouldDisplaySearch = true, {displaySignIn && } - {shouldDisplayCancelSearch && ( + {!!cancelSearch && ( { - Navigation.goBack(ROUTES.SEARCH_CENTRAL_PANE.getRoute({query: SearchQueryUtils.buildCannedSearchQuery()})); + cancelSearch(); }} > {translate('common.cancel')} diff --git a/src/components/Search/SearchAutocompleteInput.tsx b/src/components/Search/SearchAutocompleteInput.tsx index a89c68364ad9..f0f3468a9481 100644 --- a/src/components/Search/SearchAutocompleteInput.tsx +++ b/src/components/Search/SearchAutocompleteInput.tsx @@ -1,9 +1,10 @@ +/* eslint-disable rulesdir/no-acc-spread-in-reduce */ import type {ForwardedRef, ReactNode, RefObject} from 'react'; -import React, {forwardRef, useCallback, useEffect, useLayoutEffect, useMemo, useState} from 'react'; +import React, {forwardRef, useCallback, useEffect, useLayoutEffect, useMemo} from 'react'; import {View} from 'react-native'; import type {StyleProp, TextInputProps, ViewStyle} from 'react-native'; import {useOnyx} from 'react-native-onyx'; -import {useSharedValue} from 'react-native-reanimated'; +import Animated, {LinearTransition, useAnimatedStyle, useSharedValue} from 'react-native-reanimated'; import FormHelpMessage from '@components/FormHelpMessage'; import type {SelectionListHandle} from '@components/SelectionList/types'; import TextInput from '@components/TextInput'; @@ -12,6 +13,7 @@ import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useCurrentUserPersonalDetails from '@hooks/useCurrentUserPersonalDetails'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; +import useTheme from '@hooks/useTheme'; import useThemeStyles from '@hooks/useThemeStyles'; import {parseFSAttributes} from '@libs/Fullstory'; import runOnLiveMarkdownRuntime from '@libs/runOnLiveMarkdownRuntime'; @@ -52,10 +54,10 @@ type SearchAutocompleteInputProps = { onBlur?: () => void; /** Any additional styles to apply */ - wrapperStyle?: StyleProp; + wrapperStyle?: ViewStyle; /** Any additional styles to apply when input is focused */ - wrapperFocusedStyle?: StyleProp; + wrapperFocusedStyle?: ViewStyle; /** Any additional styles to apply to text input along with FormHelperMessage */ outerWrapperStyle?: StyleProp; @@ -84,7 +86,7 @@ function SearchAutocompleteInput( onBlur, caretHidden = false, wrapperStyle, - wrapperFocusedStyle, + wrapperFocusedStyle = {}, outerWrapperStyle, rightComponent, isSearchingForReports, @@ -94,8 +96,8 @@ function SearchAutocompleteInput( ref: ForwardedRef, ) { const styles = useThemeStyles(); + const theme = useTheme(); const {translate} = useLocalize(); - const [isFocused, setIsFocused] = useState(false); const {isOffline} = useNetwork(); const {activeWorkspaceID} = useActiveWorkspace(); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); @@ -122,6 +124,12 @@ function SearchAutocompleteInput( const offlineMessage: string = isOffline && shouldShowOfflineMessage ? `${translate('common.youAppearToBeOffline')} ${translate('search.resultsAreLimited')}` : ''; + // we are handling focused/unfocused style using shared value instead of using state to avoid re-rendering. Otherwise layout animation in `Animated.View` will lag. + const focusedSharedValue = useSharedValue(false); + const wrapperAnimatedStyle = useAnimatedStyle(() => { + return focusedSharedValue.get() ? wrapperFocusedStyle : wrapperStyle ?? {}; + }); + useEffect(() => { runOnLiveMarkdownRuntime(() => { 'worklet'; @@ -170,7 +178,10 @@ function SearchAutocompleteInput( return ( - + { - setIsFocused(true); autocompleteListRef?.current?.updateExternalTextInputFocus(true); + focusedSharedValue.set(true); onFocus?.(); }} onBlur={() => { - setIsFocused(false); autocompleteListRef?.current?.updateExternalTextInputFocus(false); + focusedSharedValue.set(false); onBlur?.(); }} isLoading={!!isSearchingForReports} @@ -216,8 +228,15 @@ function SearchAutocompleteInput( selection={selection} /> - {!!rightComponent && {rightComponent}} - + {!!rightComponent && ( + + {rightComponent} + + )} + mergeCardListWithWorkspaceFeeds(workspaceCardFeeds, userCardList), [userCardList, workspaceCardFeeds]); + const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST); + const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST); + const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]); const cardAutocompleteList = Object.values(allCards); const participantsAutocompleteList = useMemo(() => { diff --git a/src/components/Search/SearchPageHeader.tsx b/src/components/Search/SearchPageHeader/SearchPageHeader.tsx similarity index 75% rename from src/components/Search/SearchPageHeader.tsx rename to src/components/Search/SearchPageHeader/SearchPageHeader.tsx index d9c2d44adb6b..9f68e504abc3 100644 --- a/src/components/Search/SearchPageHeader.tsx +++ b/src/components/Search/SearchPageHeader/SearchPageHeader.tsx @@ -10,6 +10,8 @@ import DecisionModal from '@components/DecisionModal'; import * as Expensicons from '@components/Icon/Expensicons'; import {usePersonalDetails} from '@components/OnyxProvider'; import {useProductTrainingContext} from '@components/ProductTrainingContext'; +import {useSearchContext} from '@components/Search/SearchContext'; +import type {PaymentData, SearchQueryJSON} from '@components/Search/types'; import EducationalTooltip from '@components/Tooltip/EducationalTooltip'; import useActiveWorkspace from '@hooks/useActiveWorkspace'; import useLocalize from '@hooks/useLocalize'; @@ -28,22 +30,20 @@ import { import {mergeCardListWithWorkspaceFeeds} from '@libs/CardUtils'; import Navigation from '@libs/Navigation/Navigation'; import {getAllTaxRates, hasVBBA} from '@libs/PolicyUtils'; -import {buildFilterFormValuesFromQuery, isCannedSearchQuery} from '@libs/SearchQueryUtils'; +import {buildFilterFormValuesFromQuery} from '@libs/SearchQueryUtils'; import SearchSelectedNarrow from '@pages/Search/SearchSelectedNarrow'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; import type DeepValueOf from '@src/types/utils/DeepValueOf'; -import {useSearchContext} from './SearchContext'; import SearchPageHeaderInput from './SearchPageHeaderInput'; -import type {PaymentData, SearchQueryJSON} from './types'; -type SearchPageHeaderProps = {queryJSON: SearchQueryJSON}; +type SearchPageHeaderProps = {queryJSON: SearchQueryJSON; searchName?: string; searchRouterListVisible?: boolean; onSearchRouterFocus?: () => void}; type SearchHeaderOptionValue = DeepValueOf | undefined; -function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { +function SearchPageHeader({queryJSON, searchName, searchRouterListVisible, onSearchRouterFocus}: SearchPageHeaderProps) { const {translate} = useLocalize(); const theme = useTheme(); const styles = useThemeStyles(); @@ -57,9 +57,9 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { const personalDetails = usePersonalDetails(); const [reports] = useOnyx(ONYXKEYS.COLLECTION.REPORT); const taxRates = getAllTaxRates(); - const [userCardList = {}] = useOnyx(ONYXKEYS.CARD_LIST); - const [workspaceCardFeeds = {}] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST); - const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds, userCardList), [userCardList, workspaceCardFeeds]); + const [userCardList] = useOnyx(ONYXKEYS.CARD_LIST); + const [workspaceCardFeeds] = useOnyx(ONYXKEYS.COLLECTION.WORKSPACE_CARDS_LIST); + const allCards = useMemo(() => mergeCardListWithWorkspaceFeeds(workspaceCardFeeds ?? CONST.EMPTY_OBJECT, userCardList), [userCardList, workspaceCardFeeds]); const [currencyList = {}] = useOnyx(ONYXKEYS.CURRENCY_LIST); const [policyCategories] = useOnyx(ONYXKEYS.COLLECTION.POLICY_CATEGORIES); const [policyTagsLists] = useOnyx(ONYXKEYS.COLLECTION.POLICY_TAGS); @@ -67,7 +67,6 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { const [isDeleteExpensesConfirmModalVisible, setIsDeleteExpensesConfirmModalVisible] = useState(false); const [isOfflineModalVisible, setIsOfflineModalVisible] = useState(false); const [isDownloadErrorModalVisible, setIsDownloadErrorModalVisible] = useState(false); - const [isScreenFocused, setIsScreenFocused] = useState(false); const {renderProductTrainingTooltip, shouldShowProductTrainingTooltip, hideProductTrainingTooltip} = useProductTrainingContext( @@ -315,94 +314,107 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) { styles.textWrap, ]); - if (shouldUseNarrowLayout) { - if (selectionMode?.isEnabled) { - return ( - - - { - setIsDeleteExpensesConfirmModalVisible(false); - }} - title={translate('iou.deleteExpense', {count: selectedTransactionsKeys.length})} - prompt={translate('iou.deleteConfirmation', {count: selectedTransactionsKeys.length})} - confirmText={translate('common.delete')} - cancelText={translate('common.cancel')} - danger - /> - setIsOfflineModalVisible(false)} - secondOptionText={translate('common.buttonConfirm')} - isVisible={isOfflineModalVisible} - onClose={() => setIsOfflineModalVisible(false)} - /> - setIsDownloadErrorModalVisible(false)} - secondOptionText={translate('common.buttonConfirm')} - isVisible={isDownloadErrorModalVisible} - onClose={() => setIsDownloadErrorModalVisible(false)} - /> - - ); - } - return null; - } - - const onFiltersButtonPress = () => { + const onFiltersButtonPress = useCallback(() => { hideProductTrainingTooltip(); const filterFormValues = buildFilterFormValuesFromQuery(queryJSON, policyCategories, policyTagsLists, currencyList, personalDetails, allCards, reports, taxRates); updateAdvancedFilters(filterFormValues); Navigation.navigate(ROUTES.SEARCH_ADVANCED_FILTERS); - }; + }, [allCards, currencyList, hideProductTrainingTooltip, personalDetails, policyCategories, policyTagsLists, queryJSON, reports, taxRates]); + + const InputRightComponent = useMemo(() => { + return headerButtonsOptions.length > 0 ? ( + null} + shouldAlwaysShowDropdownMenu + buttonSize={CONST.DROPDOWN_BUTTON_SIZE.MEDIUM} + customText={translate('workspace.common.selected', {count: selectedTransactionsKeys.length})} + options={headerButtonsOptions} + isSplitButton={false} + shouldUseStyleUtilityForAnchorPosition + /> + ) : ( + +