Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Fix: different behavior when using app and device back button in selection mode #46937

Merged
merged 29 commits into from
Feb 4, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
c1afaca
fix back button in the selection mode
nkdengineer Aug 1, 2024
f617f88
Merge branch 'main' into fix/45896
nkdengineer Aug 7, 2024
53ea108
remove useless code
nkdengineer Aug 7, 2024
4555031
Merge branch 'main' into fix/45896
nkdengineer Aug 15, 2024
cb4ccdb
Merge branch 'main' into fix/45896
nkdengineer Aug 16, 2024
097112c
Merge branch 'main' into fix/45896
nkdengineer Aug 19, 2024
76efce7
merge main
nkdengineer Aug 28, 2024
0a69dd9
resolve conflict
nkdengineer Sep 9, 2024
4a6cac6
merge main
nkdengineer Sep 25, 2024
e7bc518
merge main
nkdengineer Oct 2, 2024
3b91615
implement for mWeb
nkdengineer Oct 2, 2024
efb3993
Merge branch 'main' into fix/45896
nkdengineer Oct 2, 2024
2c5187b
merge main
nkdengineer Oct 10, 2024
093b579
merge main
nkdengineer Oct 11, 2024
39dee67
fix conflict
nkdengineer Nov 11, 2024
ce2eb7d
Merge branch 'main' into fix/45896
nkdengineer Nov 12, 2024
9201fd5
remove change on web
nkdengineer Nov 12, 2024
952539c
merge main
nkdengineer Dec 10, 2024
9d63337
Merge branch 'main' into fix/45896
nkdengineer Dec 19, 2024
3e2e006
Merge branch 'main' into fix/45896
nkdengineer Dec 23, 2024
76c3a61
fix lint
nkdengineer Dec 23, 2024
7e11167
Merge branch 'main' into fix/45896
nkdengineer Jan 3, 2025
0793384
Merge branch 'main' into fix/45896
nkdengineer Jan 15, 2025
ecdc785
Merge branch 'main' into fix/45896
nkdengineer Jan 24, 2025
18cbbe9
fix lint
nkdengineer Jan 24, 2025
3dfac09
update type
nkdengineer Jan 24, 2025
9135bd0
fix typecheck
nkdengineer Jan 24, 2025
a6d2a1a
Merge branch 'main' into fix/45896
nkdengineer Feb 3, 2025
4a708bf
Merge branch 'main' into fix/45896
nkdengineer Feb 4, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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;