Skip to content

Commit

Permalink
Merge pull request #46937 from nkdengineer/fix/45896
Browse files Browse the repository at this point in the history
Fix: different behavior when using app and device back button in selection mode
  • Loading branch information
Julesssss authored Feb 4, 2025
2 parents 6d003de + 4a708bf commit cd03f19
Show file tree
Hide file tree
Showing 9 changed files with 100 additions and 40 deletions.
14 changes: 9 additions & 5 deletions src/components/Search/SearchContext.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import React, {useCallback, useContext, useMemo, useState} from 'react';
import type {ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import {isMoneyRequestReport} from '@libs/ReportUtils';
import * as SearchUIUtils from '@libs/SearchUIUtils';
import {isReportListItemType} from '@libs/SearchUIUtils';
import CONST from '@src/CONST';
import type ChildrenProps from '@src/types/utils/ChildrenProps';
import type {SearchContext, SelectedTransactions} from './types';

const defaultSearchContext = {
currentSearchHash: -1,
shouldTurnOffSelectionMode: false,
selectedTransactions: {},
selectedReports: [],
setCurrentSearchHash: () => {},
Expand All @@ -25,17 +26,18 @@ function getReportsFromSelectedTransactions(data: TransactionListItemType[] | Re
return (data ?? [])
.filter(
(item): item is ReportListItemType =>
SearchUIUtils.isReportListItemType(item) &&
isReportListItemType(item) &&
isMoneyRequestReport(item) &&
item?.transactions?.every((transaction: {keyForList: string | number}) => selectedTransactions[transaction.keyForList]?.isSelected),
)
.map((item) => ({reportID: item.reportID, action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, total: item.total ?? 0, policyID: item.policyID ?? ''}));
.map((item) => ({reportID: item.reportID, action: item.action ?? CONST.SEARCH.ACTION_TYPES.VIEW, total: item.total ?? CONST.DEFAULT_NUMBER_ID, policyID: item.policyID}));
}

function SearchContextProvider({children}: ChildrenProps) {
const [searchContextData, setSearchContextData] = useState<Pick<SearchContext, 'currentSearchHash' | 'selectedTransactions' | 'selectedReports'>>({
const [searchContextData, setSearchContextData] = useState<Pick<SearchContext, 'currentSearchHash' | 'selectedTransactions' | 'shouldTurnOffSelectionMode' | 'selectedReports'>>({
currentSearchHash: defaultSearchContext.currentSearchHash,
selectedTransactions: defaultSearchContext.selectedTransactions,
shouldTurnOffSelectionMode: false,
selectedReports: defaultSearchContext.selectedReports,
});

Expand All @@ -53,17 +55,19 @@ function SearchContextProvider({children}: ChildrenProps) {
setSearchContextData((prevState) => ({
...prevState,
selectedTransactions,
shouldTurnOffSelectionMode: false,
selectedReports,
}));
}, []);

const clearSelectedTransactions = useCallback(
(searchHash?: number) => {
(searchHash?: number, shouldTurnOffSelectionMode = false) => {
if (searchHash === searchContextData.currentSearchHash) {
return;
}
setSearchContextData((prevState) => ({
...prevState,
shouldTurnOffSelectionMode,
selectedTransactions: {},
selectedReports: [],
}));
Expand Down
2 changes: 1 addition & 1 deletion src/components/Search/SearchPageHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ function SearchPageHeader({queryJSON}: SearchPageHeaderProps) {

const paymentData = (
selectedReports.length
? selectedReports.map((report) => ({reportID: report.reportID, amount: report.total, paymentType: lastPaymentMethods[report.policyID]}))
? selectedReports.map((report) => ({reportID: report.reportID, amount: report.total, paymentType: lastPaymentMethods[`${report.policyID}`]}))
: Object.values(selectedTransactions).map((transaction) => ({
reportID: transaction.reportID,
amount: transaction.amount,
Expand Down
86 changes: 55 additions & 31 deletions src/components/Search/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,16 +15,25 @@ import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll';
import useThemeStyles from '@hooks/useThemeStyles';
import {turnOffMobileSelectionMode, turnOnMobileSelectionMode} from '@libs/actions/MobileSelectionMode';
import * as SearchActions from '@libs/actions/Search';
import * as DeviceCapabilities from '@libs/DeviceCapabilities';
import {createTransactionThread, search} from '@libs/actions/Search';
import {canUseTouchScreen} from '@libs/DeviceCapabilities';
import Log from '@libs/Log';
import memoize from '@libs/memoize';
import isSearchTopmostCentralPane from '@libs/Navigation/isSearchTopmostCentralPane';
import type {PlatformStackNavigationProp} from '@libs/Navigation/PlatformStackNavigation/types';
import * as ReportUtils from '@libs/ReportUtils';
import * as SearchQueryUtils from '@libs/SearchQueryUtils';
import * as SearchUIUtils from '@libs/SearchUIUtils';
import * as TransactionUtils from '@libs/TransactionUtils';
import {generateReportID} from '@libs/ReportUtils';
import {buildSearchQueryString} from '@libs/SearchQueryUtils';
import {
getListItem,
getSections,
getSortedSections,
isReportActionListItemType,
isReportListItemType,
isSearchResultsEmpty as isSearchResultsEmptyUtil,
isTransactionListItemType,
shouldShowYear as shouldShowYearUtil,
} from '@libs/SearchUIUtils';
import {isOnHold} from '@libs/TransactionUtils';
import Navigation from '@navigation/Navigation';
import type {AuthScreensParamList} from '@navigation/types';
import EmptySearchView from '@pages/Search/EmptySearchView';
Expand Down Expand Up @@ -57,7 +66,7 @@ function mapTransactionItemToSelectedEntry(item: TransactionListItemType): [stri
isSelected: true,
canDelete: item.canDelete,
canHold: item.canHold,
isHeld: TransactionUtils.isOnHold(item),
isHeld: isOnHold(item),
canUnhold: item.canUnhold,
action: item.action,
reportID: item.reportID,
Expand All @@ -77,14 +86,14 @@ function mapToItemWithSelectionInfo(
canSelectMultiple: boolean,
shouldAnimateInHighlight: boolean,
) {
if (SearchUIUtils.isReportActionListItemType(item)) {
if (isReportActionListItemType(item)) {
return {
...item,
shouldAnimateInHighlight,
};
}

return SearchUIUtils.isTransactionListItemType(item)
return isTransactionListItemType(item)
? mapToTransactionItemWithSelectionInfo(item, selectedTransactions, canSelectMultiple, shouldAnimateInHighlight)
: {
...item,
Expand All @@ -107,7 +116,7 @@ function prepareTransactionsList(item: TransactionListItemType, selectedTransact
isSelected: true,
canDelete: item.canDelete,
canHold: item.canHold,
isHeld: TransactionUtils.isOnHold(item),
isHeld: isOnHold(item),
canUnhold: item.canUnhold,
action: item.action,
reportID: item.reportID,
Expand All @@ -127,8 +136,16 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
const navigation = useNavigation<PlatformStackNavigationProp<AuthScreensParamList>>();
const isFocused = useIsFocused();
const [lastNonEmptySearchResults, setLastNonEmptySearchResults] = useState<SearchResults | undefined>(undefined);
const {setCurrentSearchHash, setSelectedTransactions, selectedTransactions, clearSelectedTransactions, setShouldShowStatusBarLoading, lastSearchType, setLastSearchType} =
useSearchContext();
const {
setCurrentSearchHash,
setSelectedTransactions,
selectedTransactions,
clearSelectedTransactions,
shouldTurnOffSelectionMode,
setShouldShowStatusBarLoading,
lastSearchType,
setLastSearchType,
} = useSearchContext();
const {selectionMode} = useMobileSelectionMode(false);
const [offset, setOffset] = useState(0);

Expand Down Expand Up @@ -158,6 +175,13 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
setCurrentSearchHash(hash);
}, [hash, clearSelectedTransactions, setCurrentSearchHash]);

useEffect(() => {
const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]);
if (selectedKeys.length === 0 && selectionMode?.isEnabled && shouldTurnOffSelectionMode) {
turnOffMobileSelectionMode();
}
}, [selectedTransactions, selectionMode?.isEnabled, shouldTurnOffSelectionMode]);

useEffect(() => {
const selectedKeys = Object.keys(selectedTransactions).filter((key) => selectedTransactions[key]);
if (!isSmallScreenWidth) {
Expand All @@ -176,12 +200,12 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
return;
}

SearchActions.search({queryJSON, offset});
search({queryJSON, offset});
}, [isOffline, offset, queryJSON]);

const getItemHeight = useCallback(
(item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
if (SearchUIUtils.isTransactionListItemType(item) || SearchUIUtils.isReportActionListItemType(item)) {
if (isTransactionListItemType(item) || isReportActionListItemType(item)) {
return isLargeScreenWidth ? variables.optionRowHeight + listItemPadding : transactionItemMobileHeight + listItemPadding;
}

Expand Down Expand Up @@ -229,14 +253,14 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo

const shouldShowLoadingState = !isOffline && !isDataLoaded;
const shouldShowLoadingMoreItems = !shouldShowLoadingState && searchResults?.search?.isLoading && searchResults?.search?.offset > 0;
const isSearchResultsEmpty = !searchResults?.data || SearchUIUtils.isSearchResultsEmpty(searchResults);
const isSearchResultsEmpty = !searchResults?.data || isSearchResultsEmptyUtil(searchResults);
const prevIsSearchResultEmpty = usePrevious(isSearchResultsEmpty);

const data = useMemo(() => {
if (searchResults === undefined) {
return [];
}
return SearchUIUtils.getSections(type, status, searchResults.data, searchResults.search);
return getSections(type, status, searchResults.data, searchResults.search);
}, [searchResults, status, type]);

useEffect(() => {
Expand All @@ -260,7 +284,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
newTransactionList[transaction.transactionID] = {
action: transaction.action,
canHold: transaction.canHold,
isHeld: TransactionUtils.isOnHold(transaction),
isHeld: isOnHold(transaction),
canUnhold: transaction.canUnhold,
isSelected: selectedTransactions[transaction.transactionID].isSelected,
canDelete: transaction.canDelete,
Expand All @@ -281,7 +305,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
newTransactionList[transaction.transactionID] = {
action: transaction.action,
canHold: transaction.canHold,
isHeld: TransactionUtils.isOnHold(transaction),
isHeld: isOnHold(transaction),
canUnhold: transaction.canUnhold,
isSelected: selectedTransactions[transaction.transactionID].isSelected,
canDelete: transaction.canDelete,
Expand Down Expand Up @@ -328,8 +352,8 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
return <FullPageOfflineBlockingView>{null}</FullPageOfflineBlockingView>;
}

const ListItem = SearchUIUtils.getListItem(type, status);
const sortedData = SearchUIUtils.getSortedSections(type, status, data, sortBy, sortOrder);
const ListItem = getListItem(type, status);
const sortedData = getSortedSections(type, status, data, sortBy, sortOrder);
const isChat = type === CONST.SEARCH.DATA_TYPES.CHAT;
const sortedSelectedData = sortedData.map((item) => {
const baseKey = isChat
Expand Down Expand Up @@ -364,10 +388,10 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
}

const toggleTransaction = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
if (SearchUIUtils.isReportActionListItemType(item)) {
if (isReportActionListItemType(item)) {
return;
}
if (SearchUIUtils.isTransactionListItemType(item)) {
if (isTransactionListItemType(item)) {
if (!item.keyForList) {
return;
}
Expand Down Expand Up @@ -398,21 +422,21 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo

const openReport = (item: TransactionListItemType | ReportListItemType | ReportActionListItemType) => {
const isFromSelfDM = item.reportID === CONST.REPORT.UNREPORTED_REPORTID;
let reportID = SearchUIUtils.isTransactionListItemType(item) && (!item.isFromOneTransactionReport || isFromSelfDM) ? item.transactionThreadReportID : item.reportID;
let reportID = isTransactionListItemType(item) && (!item.isFromOneTransactionReport || isFromSelfDM) ? item.transactionThreadReportID : item.reportID;

if (!reportID) {
return;
}

// If we're trying to open a legacy transaction without a transaction thread, let's create the thread and navigate the user
if (SearchUIUtils.isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) {
reportID = ReportUtils.generateReportID();
SearchActions.createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID);
if (isTransactionListItemType(item) && reportID === '0' && item.moneyRequestReportActionID) {
reportID = generateReportID();
createTransactionThread(hash, item.transactionID, reportID, item.moneyRequestReportActionID);
}

const backTo = Navigation.getActiveRoute();

if (SearchUIUtils.isReportActionListItemType(item)) {
if (isReportActionListItemType(item)) {
const reportActionID = item.reportActionID;
Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, reportActionID, backTo}));
return;
Expand Down Expand Up @@ -448,11 +472,11 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
};

const onSortPress = (column: SearchColumnType, order: SortOrder) => {
const newQuery = SearchQueryUtils.buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order});
const newQuery = buildSearchQueryString({...queryJSON, sortBy: column, sortOrder: order});
navigation.setParams({q: newQuery});
};

const shouldShowYear = SearchUIUtils.shouldShowYear(searchResults?.data);
const shouldShowYear = shouldShowYearUtil(searchResults?.data);
const shouldShowSorting = !Array.isArray(status) && sortableSearchStatuses.includes(status);

return (
Expand All @@ -477,7 +501,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
)
}
isSelected={(item) =>
status !== CONST.SEARCH.STATUS.EXPENSE.ALL && SearchUIUtils.isReportListItemType(item)
status !== CONST.SEARCH.STATUS.EXPENSE.ALL && isReportListItemType(item)
? item.transactions.some((transaction) => selectedTransactions[transaction.keyForList]?.isSelected)
: !!item.isSelected
}
Expand All @@ -501,7 +525,7 @@ function Search({queryJSON, onSearchListScroll, isSearchScreenFocused, contentCo
onSelectRow={openReport}
getItemHeight={getItemHeightMemoized}
shouldSingleExecuteRowSelect
shouldPreventDefaultFocusOnSelectRow={!DeviceCapabilities.canUseTouchScreen()}
shouldPreventDefaultFocusOnSelectRow={!canUseTouchScreen()}
shouldPreventDefault={false}
listHeaderWrapperStyle={[styles.ph8, styles.pt3]}
containerStyle={[styles.pv0, type === CONST.SEARCH.DATA_TYPES.CHAT && !isSmallScreenWidth && styles.pt3]}
Expand Down
5 changes: 3 additions & 2 deletions src/components/Search/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type SelectedTransactions = Record<string, SelectedTransactionInfo>;
/** Model of selected reports */
type SelectedReports = {
reportID: string;
policyID: string;
policyID: string | undefined;
action: ValueOf<typeof CONST.SEARCH.ACTION_TYPES>;
total: number;
};
Expand All @@ -65,7 +65,8 @@ type SearchContext = {
selectedReports: SelectedReports[];
setCurrentSearchHash: (hash: number) => void;
setSelectedTransactions: (selectedTransactions: SelectedTransactions, data: TransactionListItemType[] | ReportListItemType[] | ReportActionListItemType[]) => void;
clearSelectedTransactions: (hash?: number) => void;
clearSelectedTransactions: (hash?: number, shouldTurnOffSelectionMode?: boolean) => void;
shouldTurnOffSelectionMode: boolean;
shouldShowStatusBarLoading: boolean;
setShouldShowStatusBarLoading: (shouldShow: boolean) => void;
setLastSearchType: (type: string | undefined) => void;
Expand Down
2 changes: 1 addition & 1 deletion src/libs/PolicyUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1122,7 +1122,7 @@ function getWorkspaceAccountID(policyID?: string) {
return policy.workspaceAccountID ?? CONST.DEFAULT_NUMBER_ID;
}

function hasVBBA(policyID: string) {
function hasVBBA(policyID: string | undefined) {
const policy = getPolicy(policyID);
return !!policy?.achAccount?.bankAccountID;
}
Expand Down
13 changes: 13 additions & 0 deletions src/pages/Search/SearchPageBottomTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import Animated, {clamp, useAnimatedScrollHandler, useAnimatedStyle, useSharedVa
import FullPageNotFoundView from '@components/BlockingViews/FullPageNotFoundView';
import ScreenWrapper from '@components/ScreenWrapper';
import Search from '@components/Search';
import {useSearchContext} from '@components/Search/SearchContext';
import SearchStatusBar from '@components/Search/SearchStatusBar';
import useActiveCentralPaneRoute from '@hooks/useActiveCentralPaneRoute';
import useLocalize from '@hooks/useLocalize';
Expand All @@ -22,6 +23,7 @@ import ROUTES from '@src/ROUTES';
import SCREENS from '@src/SCREENS';
import SearchSelectionModeHeader from './SearchSelectionModeHeader';
import SearchTypeMenu from './SearchTypeMenu';
import useHandleBackButton from './useHandleBackButton';

const TOO_CLOSE_TO_TOP_DISTANCE = 10;
const TOO_CLOSE_TO_BOTTOM_DISTANCE = 10;
Expand All @@ -35,6 +37,17 @@ function SearchPageBottomTab() {
const styles = useThemeStyles();
const StyleUtils = useStyleUtils();
const [selectionMode] = useOnyx(ONYXKEYS.MOBILE_SELECTION_MODE);
const {clearSelectedTransactions} = useSearchContext();

const handleBackButtonPress = useCallback(() => {
if (!selectionMode?.isEnabled) {
return false;
}
clearSelectedTransactions(undefined, true);
return true;
}, [selectionMode, clearSelectedTransactions]);

useHandleBackButton(handleBackButtonPress);

const scrollOffset = useSharedValue(0);
const topBarOffset = useSharedValue<number>(StyleUtils.searchHeaderHeight);
Expand Down
11 changes: 11 additions & 0 deletions src/pages/Search/useHandleBackButton/index.android.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {useEffect} from 'react';
import {BackHandler} from 'react-native';
import type UseHandleBackButtonCallback from './type';

export default function useHandleBackButton(callback: UseHandleBackButtonCallback) {
useEffect(() => {
const backHandler = BackHandler.addEventListener('hardwareBackPress', callback);

return () => backHandler.remove();
}, [callback]);
}
4 changes: 4 additions & 0 deletions src/pages/Search/useHandleBackButton/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import type UseHandleBackButtonCallback from './type';

// eslint-disable-next-line @typescript-eslint/no-unused-vars
export default function useHandleBackButton(_callback: UseHandleBackButtonCallback) {}
3 changes: 3 additions & 0 deletions src/pages/Search/useHandleBackButton/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
type UseHandleBackButtonCallback = () => boolean;

export default UseHandleBackButtonCallback;

0 comments on commit cd03f19

Please sign in to comment.