From 0040f0d68cde71af787b6bcb86972f83a0a13254 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Tue, 11 Mar 2025 15:25:06 +0100 Subject: [PATCH 1/8] Add basic version of MoneyRequestReportView with basic transaction list and chat --- src/CONST.ts | 5 + .../MoneyRequestReportTableHeader.tsx | 90 ++++++++ .../MoneyRequestReportTransactionList.tsx | 52 +++++ .../MoneyRequestReportView.tsx | 196 ++++++++++++++++++ src/components/Search/index.tsx | 7 + .../SelectionList/Search/ReportListItem.tsx | 2 +- .../Search/TransactionListItemRow.tsx | 22 +- .../SelectionList/SearchTableHeader.tsx | 100 +++++---- .../SelectionList/SortableTableHeader.tsx | 66 ++++++ src/components/TransactionItemRow/index.tsx | 14 +- src/libs/SearchUIUtils.ts | 3 +- .../Search/SearchMoneyRequestReportPage.tsx | 109 ++++++---- src/pages/home/ReportScreen.tsx | 2 +- .../home/report/PureReportActionItem.tsx | 1 + .../report/ReportActionsListItemRenderer.tsx | 2 +- src/styles/utils/index.ts | 4 +- 16 files changed, 557 insertions(+), 118 deletions(-) create mode 100644 src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx create mode 100644 src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx create mode 100644 src/components/MoneyRequestReportView/MoneyRequestReportView.tsx create mode 100644 src/components/SelectionList/SortableTableHeader.tsx diff --git a/src/CONST.ts b/src/CONST.ts index 0ef02fcbd328f..8adaa4a82e725 100755 --- a/src/CONST.ts +++ b/src/CONST.ts @@ -1321,6 +1321,11 @@ const CONST = { }, THREAD_DISABLED: ['CREATED'], }, + TRANSACTION_LIST: { + COLUMNS: { + COMMENTS: 'comments', + }, + }, CANCEL_PAYMENT_REASONS: { ADMIN: 'CANCEL_REASON_ADMIN', }, diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx new file mode 100644 index 0000000000000..d34c3fb82eb34 --- /dev/null +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx @@ -0,0 +1,90 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {SearchColumnType, SortOrder} from '@components/Search/types'; +import SortableTableHeader from '@components/SelectionList/SortableTableHeader'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; + +type ColumnConfig = { + columnName: SearchColumnType | typeof CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS; + translationKey: TranslationPaths | undefined; + isColumnSortable?: boolean; +}; + +const columnConfig: ColumnConfig[] = [ + { + columnName: CONST.SEARCH.TABLE_COLUMNS.RECEIPT, + translationKey: 'common.receipt', + isColumnSortable: false, + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.TYPE, + translationKey: 'common.type', + isColumnSortable: false, + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.DATE, + translationKey: 'common.date', + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.MERCHANT, + translationKey: 'common.merchant', + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.CATEGORY, + translationKey: 'common.category', + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.TAG, + translationKey: 'common.tag', + }, + { + columnName: CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS, + translationKey: undefined, // comments have no title displayed + isColumnSortable: false, + }, + { + columnName: CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT, + translationKey: 'common.total', + }, +]; + +type SearchTableHeaderProps = { + sortBy?: SearchColumnType; + sortOrder?: SortOrder; + onSortPress: (column: SearchColumnType | typeof CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS, order: SortOrder) => void; + shouldShowSorting: boolean; +}; + +// At this moment with new Report View we have no extra logic for displaying columns +const shouldShowColumn = () => true; + +function MoneyRequestReportTableHeader({sortBy, sortOrder, onSortPress, shouldShowSorting}: SearchTableHeaderProps) { + const styles = useThemeStyles(); + const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); + const displayNarrowVersion = isMediumScreenWidth || shouldUseNarrowLayout; + + if (displayNarrowVersion) { + return; + } + + return ( + + + + ); +} + +MoneyRequestReportTableHeader.displayName = 'MoneyRequestReportTableHeader'; + +export default MoneyRequestReportTableHeader; diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx new file mode 100644 index 0000000000000..127f036f0c36b --- /dev/null +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -0,0 +1,52 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import TransactionItemRow from '@components/TransactionItemRow'; +import useThemeStyles from '@hooks/useThemeStyles'; +import type * as OnyxTypes from '@src/types/onyx'; +import MoneyRequestReportTableHeader from './MoneyRequestReportTableHeader'; + +type MoneyRequestReportTransactionListProps = { + /** The report */ + report: OnyxEntry; + + /** List of transactions belonging to one report */ + transactions: OnyxTypes.Transaction[]; +}; + +/** + * TODO + * This component is under construction and not yet displayed to any users. + */ +function MoneyRequestReportTransactionList({report, transactions}: MoneyRequestReportTransactionListProps) { + const styles = useThemeStyles(); + + return ( + <> + {}} + /> + + {transactions.map((transaction) => { + return ( + + + + ); + })} + + + ); +} + +MoneyRequestReportTransactionList.displayName = 'MoneyRequestReportTransactionList'; + +export default MoneyRequestReportTransactionList; diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx new file mode 100644 index 0000000000000..a5290939b75ea --- /dev/null +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -0,0 +1,196 @@ +import type {ListRenderItemInfo} from '@react-native/virtualized-lists/Lists/VirtualizedList'; +import React, {useCallback, useMemo} from 'react'; +import {View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; +import FlatList from '@components/FlatList'; +import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; +import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; +import useThemeStyles from '@hooks/useThemeStyles'; +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import { + getMostRecentIOURequestActionID, + getOneTransactionThreadReportID, + getSortedReportActionsForDisplay, + isConsecutiveActionMadeByPreviousActor, + isConsecutiveChronosAutomaticTimerAction, + isDeletedParentAction, + shouldReportActionBeVisible, +} from '@libs/ReportActionsUtils'; +import {canUserPerformWriteAction, chatIncludesChronosWithID} from '@libs/ReportUtils'; +import ReportActionsListItemRenderer from '@pages/home/report/ReportActionsListItemRenderer'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type Transaction from '@src/types/onyx/Transaction'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import MoneyRequestReportTransactionList from './MoneyRequestReportTransactionList'; + +type TemporaryMoneyRequestReportViewProps = { + /** The report */ + report: OnyxEntry; +}; + +function getParentReportAction(parentReportActions: OnyxEntry, parentReportActionID: string | undefined): OnyxEntry { + if (!parentReportActions || !parentReportActionID) { + return; + } + return parentReportActions[parentReportActionID]; +} + +/** + * TODO + * This component is under construction and not yet displayed to any users. + */ +function MoneyRequestReportView({report}: TemporaryMoneyRequestReportViewProps) { + const styles = useThemeStyles(); + + const reportID = report?.reportID; + + // const [accountManagerReportID] = useOnyx(ONYXKEYS.ACCOUNT_MANAGER_REPORT_ID); + // const [accountManagerReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(accountManagerReportID)}`); + // const [userLeavingStatus = false] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportID}`); + const [reportOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {allowStaleData: true}); + // const [reportNameValuePairsOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`, {allowStaleData: true}); + const [parentReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(reportOnyx?.parentReportID)}`, { + canEvict: false, + selector: (parentReportActions) => getParentReportAction(parentReportActions, reportOnyx?.parentReportActionID), + }); + + const {reportActions, linkedAction, sortedAllReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID); + + const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]); + + const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], false); + const [transactionThreadReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`, { + selector: (actions: OnyxEntry) => getSortedReportActionsForDisplay(actions, canUserPerformWriteAction(report), true), + }); + + const [transactions = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); + + const isOffline = false; + + const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`); + + const actionsChatItem = reportActions.filter((ra) => { + return ra.actionName !== 'IOU' && ra.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED; + }); + + // // Get a sorted array of reportActions for both the current report and the transaction thread report associated with this report (if there is one) + // // so that we display transaction-level and report-level report actions in order in the one-transaction view + // const reportActions2 = useMemo( + // () => (reportActionsToDisplay ? getCombinedReportActions(reportActionsToDisplay, transactionThreadReportID ?? null, transactionThreadReportActions ?? []) : []), + // [reportActionsToDisplay, transactionThreadReportActions, transactionThreadReportID], + // ); + + /* Thread's divider line should hide when the first chat in the thread is marked as unread. + * This is so that it will not be conflicting with header's separator line. + */ + // const shouldHideThreadDividerLine = useMemo( + // (): boolean => getFirstVisibleReportActionID(reportActions, isOffline) === unreadMarkerReportActionID, + // [reportActions, isOffline, unreadMarkerReportActionID], + // ); + + const parentReportActionForTransactionThread = useMemo( + () => + isEmptyObject(transactionThreadReportActions) + ? undefined + : (reportActions?.find((action) => action.reportActionID === transactionThreadReport?.parentReportActionID) as OnyxEntry), + [reportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID], + ); + + const canPerformWriteAction = canUserPerformWriteAction(report); + const visibleReportActions = useMemo(() => { + const filteredActions = actionsChatItem.filter( + (reportAction) => + (isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && + shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction), + ); + + return [...filteredActions].toReversed(); + }, [actionsChatItem, isOffline, canPerformWriteAction]); + + const transactionList = Object.values(transactions).filter((transaction): transaction is Transaction => { + return transaction?.reportID === reportID; + }); + + const renderItem = useCallback( + ({item: reportAction, index}: ListRenderItemInfo) => ( + 1} + isFirstVisibleReportAction={false} + /> + ), + [ + report, + visibleReportActions, + mostRecentIOUReportActionID, + parentReportAction, + transactionThreadReport, + parentReportActionForTransactionThread, + // unreadMarkerReportActionID, + // shouldHideThreadDividerLine, + // shouldUseThreadDividerLine, + // firstVisibleReportActionID, + ], + ); + + const listHeaderComponent = ( + + ); + + return ( + + {report ? ( + item.reportActionID} + initialNumToRender={10} + // onEndReached={onEndReached} + onEndReachedThreshold={0.75} + // onStartReached={onStartReached} + onStartReachedThreshold={0.75} + ListHeaderComponent={listHeaderComponent} + keyboardShouldPersistTaps="handled" + // onLayout={onLayoutInner} + // onContentSizeChange={onContentSizeChangeInner} + // onScroll={trackVerticalScrolling} + // onScrollToIndexFailed={onScrollToIndexFailed} + // extraData={extraData} + // key={listID} + // shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScrollToTopThreshold} + // initialScrollKey={reportActionID} + /> + ) : ( + + )} + + ); +} + +MoneyRequestReportView.displayName = 'MoneyRequestReportView'; + +export default MoneyRequestReportView; diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx index ebeb5b8170cff..c5a10c43e76c7 100644 --- a/src/components/Search/index.tsx +++ b/src/components/Search/index.tsx @@ -10,6 +10,7 @@ import SelectionListWithModal from '@components/SelectionListWithModal'; import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import useMobileSelectionMode from '@hooks/useMobileSelectionMode'; import useNetwork from '@hooks/useNetwork'; +import usePermissions from '@hooks/usePermissions'; import usePrevious from '@hooks/usePrevious'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useSearchHighlightAndScroll from '@hooks/useSearchHighlightAndScroll'; @@ -150,6 +151,7 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS const previousReportActions = usePrevious(reportActions); const shouldGroupByReports = groupBy === CONST.SEARCH.GROUP_BY.REPORTS; + const {canUseTableReportView} = usePermissions(); const canSelectMultiple = isSmallScreenWidth ? !!selectionMode?.isEnabled : true; useEffect(() => { @@ -421,6 +423,11 @@ function Search({queryJSON, currentSearchResults, lastNonEmptySearchResults, onS const backTo = Navigation.getActiveRoute(); + if (canUseTableReportView && isReportListItemType(item)) { + Navigation.navigate(ROUTES.SEARCH_MONEY_REQUEST_REPORT.getRoute({reportID, backTo})); + return; + } + if (isReportActionListItemType(item)) { const reportActionID = item.reportActionID; Navigation.navigate(ROUTES.SEARCH_REPORT.getRoute({reportID, reportActionID, backTo})); diff --git a/src/components/SelectionList/Search/ReportListItem.tsx b/src/components/SelectionList/Search/ReportListItem.tsx index 7a573412871b4..05cc2f280ef9b 100644 --- a/src/components/SelectionList/Search/ReportListItem.tsx +++ b/src/components/SelectionList/Search/ReportListItem.tsx @@ -196,7 +196,7 @@ function ReportListItem({ {isLargeScreenWidth && ( - + )} - + - + - + - + - + - + {item.shouldShowCategory && ( - + )} {item.shouldShowTag && ( - + )} {item.shouldShowTax && ( - + )} - + - + boolean; type SearchColumnConfig = { columnName: SearchColumnType; translationKey: TranslationPaths; isColumnSortable?: boolean; - shouldShow: (data: OnyxTypes.SearchResults['data'], metadata: OnyxTypes.SearchResults['search']) => boolean; +}; + +const shouldShowColumnConfig: Record = { + [CONST.SEARCH.TABLE_COLUMNS.RECEIPT]: () => true, + [CONST.SEARCH.TABLE_COLUMNS.TYPE]: () => true, + [CONST.SEARCH.TABLE_COLUMNS.DATE]: () => true, + [CONST.SEARCH.TABLE_COLUMNS.MERCHANT]: (data: OnyxTypes.SearchResults['data']) => getShouldShowMerchant(data), + [CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION]: (data: OnyxTypes.SearchResults['data']) => !getShouldShowMerchant(data), + [CONST.SEARCH.TABLE_COLUMNS.FROM]: () => true, + [CONST.SEARCH.TABLE_COLUMNS.TO]: () => true, + [CONST.SEARCH.TABLE_COLUMNS.CATEGORY]: (data, metadata) => metadata?.columnsToShow?.shouldShowCategoryColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.TAG]: (data, metadata) => metadata?.columnsToShow?.shouldShowTagColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: (data, metadata) => metadata?.columnsToShow?.shouldShowTaxColumn ?? false, + [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: () => true, + [CONST.SEARCH.TABLE_COLUMNS.ACTION]: () => true, }; const expenseHeaders: SearchColumnConfig[] = [ { columnName: CONST.SEARCH.TABLE_COLUMNS.RECEIPT, translationKey: 'common.receipt', - shouldShow: () => true, isColumnSortable: false, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.TYPE, translationKey: 'common.type', - shouldShow: () => true, isColumnSortable: false, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.DATE, translationKey: 'common.date', - shouldShow: () => true, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.MERCHANT, translationKey: 'common.merchant', - shouldShow: (data: OnyxTypes.SearchResults['data']) => getShouldShowMerchant(data), }, { columnName: CONST.SEARCH.TABLE_COLUMNS.DESCRIPTION, translationKey: 'common.description', - shouldShow: (data: OnyxTypes.SearchResults['data']) => !getShouldShowMerchant(data), }, { columnName: CONST.SEARCH.TABLE_COLUMNS.FROM, translationKey: 'common.from', - shouldShow: () => true, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.TO, translationKey: 'common.to', - shouldShow: () => true, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.CATEGORY, translationKey: 'common.category', - shouldShow: (data, metadata) => metadata?.columnsToShow?.shouldShowCategoryColumn ?? false, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.TAG, translationKey: 'common.tag', - shouldShow: (data, metadata) => metadata?.columnsToShow?.shouldShowTagColumn ?? false, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT, translationKey: 'common.tax', - shouldShow: (data, metadata) => metadata?.columnsToShow?.shouldShowTaxColumn ?? false, isColumnSortable: false, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT, translationKey: 'common.total', - shouldShow: () => true, }, { columnName: CONST.SEARCH.TABLE_COLUMNS.ACTION, translationKey: 'common.action', - shouldShow: () => true, isColumnSortable: false, }, ]; @@ -103,52 +103,50 @@ type SearchTableHeaderProps = { }; function SearchTableHeader({data, metadata, sortBy, sortOrder, onSortPress, shouldShowYear, shouldShowSorting}: SearchTableHeaderProps) { - const styles = useThemeStyles(); - const StyleUtils = useStyleUtils(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); - const {translate} = useLocalize(); const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth; - if (SearchColumns[metadata.type] === null) { - return; - } + const shouldShowColumn = useCallback( + (columnName: SearchColumnType | typeof CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS) => { + if (columnName === CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS) { + return false; + } + + const shouldShowFun = shouldShowColumnConfig[columnName]; + return shouldShowFun(data, metadata); + }, + [data, metadata], + ); if (displayNarrowVersion) { return; } - return ( - - - {SearchColumns[metadata.type]?.map(({columnName, translationKey, shouldShow, isColumnSortable}) => { - if (!shouldShow(data, metadata)) { - return null; - } + const columnConfig = SearchColumns[metadata.type]; - const isSortable = shouldShowSorting && isColumnSortable; - const isActive = sortBy === columnName; - const textStyle = columnName === CONST.SEARCH.TABLE_COLUMNS.RECEIPT ? StyleUtils.getTextOverflowStyle('clip') : null; + if (!columnConfig) { + return; + } - return ( - onSortPress(columnName, order)} - /> - ); - })} - - + return ( + { + if (columnName === CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS) { + return; + } + onSortPress(columnName, order); + }} + /> ); } SearchTableHeader.displayName = 'SearchTableHeader'; export default SearchTableHeader; -export {SearchColumns}; diff --git a/src/components/SelectionList/SortableTableHeader.tsx b/src/components/SelectionList/SortableTableHeader.tsx new file mode 100644 index 0000000000000..b16ea1c4c0f35 --- /dev/null +++ b/src/components/SelectionList/SortableTableHeader.tsx @@ -0,0 +1,66 @@ +import React from 'react'; +import {View} from 'react-native'; +import type {SearchColumnType, SortOrder} from '@components/Search/types'; +import useLocalize from '@hooks/useLocalize'; +import useStyleUtils from '@hooks/useStyleUtils'; +import useThemeStyles from '@hooks/useThemeStyles'; +import CONST from '@src/CONST'; +import type {TranslationPaths} from '@src/languages/types'; +import SortableHeaderText from './SortableHeaderText'; + +type SortableColumnName = SearchColumnType | typeof CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS; + +type ColumnConfig = { + columnName: SortableColumnName; + translationKey: TranslationPaths | undefined; + isColumnSortable?: boolean; +}; + +type SearchTableHeaderProps = { + columns: ColumnConfig[]; + sortBy?: SortableColumnName; + sortOrder?: SortOrder; + shouldShowSorting: boolean; + dateColumnSize: 'normal' | 'wide'; + shouldShowColumn: (columnName: SortableColumnName) => boolean; + onSortPress: (column: SortableColumnName, order: SortOrder) => void; +}; + +function SortableTableHeader({columns, sortBy, sortOrder, onSortPress, shouldShowColumn, dateColumnSize, shouldShowSorting}: SearchTableHeaderProps) { + const styles = useThemeStyles(); + const StyleUtils = useStyleUtils(); + const {translate} = useLocalize(); + + return ( + + + {columns.map(({columnName, translationKey, isColumnSortable}) => { + if (!shouldShowColumn(columnName)) { + return null; + } + + const isSortable = shouldShowSorting && isColumnSortable; + const isActive = sortBy === columnName; + const textStyle = columnName === CONST.SEARCH.TABLE_COLUMNS.RECEIPT ? StyleUtils.getTextOverflowStyle('clip') : null; + + return ( + onSortPress(columnName, order)} + /> + ); + })} + + + ); +} + +SortableTableHeader.displayName = 'SortableTableHeader'; + +export default SortableTableHeader; diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index 14428f934dde1..c4e8ef0f7be76 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -91,48 +91,48 @@ function TransactionItemRow({ {(hovered) => ( - + - + - + - + - + - + - + ; - // We will figure out later, when this view is actually being finalized, how to pass down an actual query const tempJSONQuery = buildSearchQueryJSON('') as unknown as SearchQueryJSON; -type TemporaryMoneyRequestReportViewProps = { - /** The report */ - report: OnyxEntry; +type SearchPageProps = PlatformStackScreenProps; - /** The policy tied to the expense report */ - policy: OnyxEntry; +const defaultReportMetadata = { + isLoadingInitialReportActions: true, + isLoadingOlderReportActions: false, + hasLoadingOlderReportActionsError: false, + isLoadingNewerReportActions: false, + hasLoadingNewerReportActionsError: false, + isOptimisticReport: false, }; -/** - * TODO - * This is a completely temporary component, displayed to: - * - show other devs that SearchMoneyRequestReportPage works - * - unblock work for other devs for Report Creation (https://github.com/Expensify/App/issues/57654) - * - * This component is not displayed to any users. - * It will be removed once we fully implement SearchMoneyRequestReportPage (https://github.com/Expensify/App/issues/57508) - */ -function TemporaryMoneyRequestReportView({report, policy}: TemporaryMoneyRequestReportViewProps) { - const styles = useThemeStyles(); - return ( - - - { - Navigation.goBack(); - }} - /> - - ); -} - function SearchMoneyRequestReportPage({route}: SearchPageProps) { const {translate} = useLocalize(); const {shouldUseNarrowLayout} = useResponsiveLayout(); const styles = useThemeStyles(); const {reportID} = route.params; + + const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, {initialValue: false}); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {allowStaleData: true}); + const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {allowStaleData: true, initialValue: {}}); const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; + const [isComposerFocus, setIsComposerFocus] = useState(false); + + /** + * When false the ReportActionsView will completely unmount and we will show a loader until it returns true. + */ + const isCurrentReportLoadedFromOnyx = useMemo((): boolean => { + // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely + const isTransitioning = report && report?.reportID !== reportID; + return reportID !== '' && !!report?.reportID && !isTransitioning; + }, [report, reportID]); + + const onComposerFocus = useCallback(() => setIsComposerFocus(true), []); + const onComposerBlur = useCallback(() => setIsComposerFocus(false), []); + if (shouldUseNarrowLayout) { return ( - { + Navigation.goBack(); + }} /> ); @@ -106,10 +105,36 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) { - + + + + + { + Navigation.goBack(); + }} + /> + + {isCurrentReportLoadedFromOnyx ? ( + + ) : null} + + + + ); diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index f29a4d85fc56d..fd3fe04f70f1d 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -148,7 +148,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { const [userLeavingStatus] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_USER_IS_LEAVING_ROOM}${reportIDFromRoute}`, {initialValue: false}); const [reportOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportIDFromRoute}`, {allowStaleData: true}); const [reportNameValuePairsOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportIDFromRoute}`, {allowStaleData: true}); - const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`, {initialValue: defaultReportMetadata}); + const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportIDFromRoute}`); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {allowStaleData: true, initialValue: {}}); const [parentReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(reportOnyx?.parentReportID)}`, { canEvict: false, diff --git a/src/pages/home/report/PureReportActionItem.tsx b/src/pages/home/report/PureReportActionItem.tsx index 75a3ff534acd9..fdfd4b95d09c7 100644 --- a/src/pages/home/report/PureReportActionItem.tsx +++ b/src/pages/home/report/PureReportActionItem.tsx @@ -197,6 +197,7 @@ type PureReportActionItemProps = { /** Flag to show, hide the thread divider line */ shouldHideThreadDividerLine?: boolean; + /** Report action ID that was referenced in the deeplink to report */ linkedReportActionID?: string; /** Callback to be called on onPress */ diff --git a/src/pages/home/report/ReportActionsListItemRenderer.tsx b/src/pages/home/report/ReportActionsListItemRenderer.tsx index 1a0d8db136615..eaceaf1df35e2 100644 --- a/src/pages/home/report/ReportActionsListItemRenderer.tsx +++ b/src/pages/home/report/ReportActionsListItemRenderer.tsx @@ -41,7 +41,7 @@ type ReportActionsListItemRendererProps = { /** Should we display the new marker on top of the comment? */ shouldDisplayNewMarker: boolean; - /** Linked report action ID */ + /** Report action ID that was referenced in the deeplink to report */ linkedReportActionID?: string; /** Whether we should display "Replies" divider */ diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts index b6ee3085d9814..a7f7b537e359e 100644 --- a/src/styles/utils/index.ts +++ b/src/styles/utils/index.ts @@ -1617,14 +1617,14 @@ const createStyleUtils = (theme: ThemeColors, styles: ThemeStyles) => ({ return isDragging ? styles.cursorGrabbing : styles.cursorZoomOut; }, - getSearchTableColumnStyles: (columnName: string, shouldExtendDateColumn = false): ViewStyle => { + getReportTableColumnStyles: (columnName: string, isDateColumnWide = false): ViewStyle => { let columnWidth; switch (columnName) { case CONST.SEARCH.TABLE_COLUMNS.RECEIPT: columnWidth = {...getWidthStyle(variables.w36), ...styles.alignItemsCenter}; break; case CONST.SEARCH.TABLE_COLUMNS.DATE: - columnWidth = getWidthStyle(shouldExtendDateColumn ? variables.w92 : variables.w52); + columnWidth = getWidthStyle(isDateColumnWide ? variables.w92 : variables.w52); break; case CONST.SEARCH.TABLE_COLUMNS.MERCHANT: case CONST.SEARCH.TABLE_COLUMNS.FROM: From b544883dd6f246f9a02c95837cb0463195165acc Mon Sep 17 00:00:00 2001 From: Jakub Korytko Date: Fri, 14 Mar 2025 11:29:50 +0100 Subject: [PATCH 2/8] Add TransactionItemRowRBR (#211) --- .../MoneyRequestReportTransactionList.tsx | 2 +- .../TransactionItemRowRBR.tsx | 47 +++++++++++++++++++ src/components/TransactionItemRow/index.tsx | 5 +- .../Search/SearchMoneyRequestReportPage.tsx | 1 + 4 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/components/TransactionItemRow/TransactionItemRowRBR.tsx diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index 127f036f0c36b..bfe79a2491303 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -32,7 +32,7 @@ function MoneyRequestReportTransactionList({report, transactions}: MoneyRequestR {transactions.map((transaction) => { return ( - + { + const translation = ViolationsUtils.getViolationTranslation(violation, translate); + return index > 0 ? translation.charAt(0).toLowerCase() + translation.slice(1) : translation; + }) + .join(', '); + + return ( + transactionViolations.length > 0 && ( + + + + {RBRmessages} + + + ) + ); +} + +export default TransactionItemRowRBR; diff --git a/src/components/TransactionItemRow/index.tsx b/src/components/TransactionItemRow/index.tsx index c4e8ef0f7be76..e6b95e88eccdd 100644 --- a/src/components/TransactionItemRow/index.tsx +++ b/src/components/TransactionItemRow/index.tsx @@ -13,6 +13,7 @@ import ReceiptCell from './DataCells/ReceiptCell'; import TagCell from './DataCells/TagCell'; import TotalCell from './DataCells/TotalCell'; import TypeCell from './DataCells/TypeCell'; +import TransactionItemRowRBR from './TransactionItemRowRBR'; function TransactionItemRow({ transactionItem, @@ -83,13 +84,14 @@ function TransactionItemRow({ shouldUseNarrowLayout={shouldUseNarrowLayout} /> + )} ) : ( {(hovered) => ( - + + )} diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index e8d809e47cc3b..2af937a59046e 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -82,6 +82,7 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) { Navigation.goBack(); }} /> + ); } From b3875801f5de648a0ee5910199e43357f1a8c28f Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Fri, 14 Mar 2025 15:30:26 +0100 Subject: [PATCH 3/8] Fix avatars grouping working for the reverse direction of ReportView list --- src/components/MoneyReportHeader.tsx | 9 +- .../MoneyRequestReportView.tsx | 105 +++++++++--------- src/libs/ReportActionsUtils.ts | 80 ++++++++++++- .../Search/SearchMoneyRequestReportPage.tsx | 2 + 4 files changed, 137 insertions(+), 59 deletions(-) diff --git a/src/components/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx index a085385589a3b..43f64670ca99f 100644 --- a/src/components/MoneyReportHeader.tsx +++ b/src/components/MoneyReportHeader.tsx @@ -100,11 +100,14 @@ type MoneyReportHeaderProps = { // eslint-disable-next-line react/no-unused-prop-types transactionThreadReportID: string | undefined; + /** Whether back button should be displayed in header */ + shouldDisplayBackButton?: boolean; + /** Method to trigger when pressing close button of the header */ onBackButtonPress: () => void; }; -function MoneyReportHeader({policy, report: moneyRequestReport, transactionThreadReportID, reportActions, onBackButtonPress}: MoneyReportHeaderProps) { +function MoneyReportHeader({policy, report: moneyRequestReport, transactionThreadReportID, reportActions, shouldDisplayBackButton = false, onBackButtonPress}: MoneyReportHeaderProps) { // We need to use isSmallScreenWidth instead of shouldUseNarrowLayout to use a correct layout for the hold expense modal https://github.com/Expensify/App/pull/47990#issuecomment-2362382026 // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {shouldUseNarrowLayout, isSmallScreenWidth} = useResponsiveLayout(); @@ -377,6 +380,8 @@ function MoneyReportHeader({policy, report: moneyRequestReport, transactionThrea setIsDeleteRequestModalVisible(false); }, [canDeleteRequest]); + const shouldShowBackButton = shouldDisplayBackButton || shouldUseNarrowLayout; + return ( getParentReportAction(parentReportActions, reportOnyx?.parentReportActionID), }); - const {reportActions, linkedAction, sortedAllReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID); + const { + reportActions, + // linkedAction, Todo - do I need this? + // sortedAllReportActions, + // hasNewerActions, + // hasOlderActions + } = usePaginatedReportActions(reportID); const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]); @@ -72,18 +77,8 @@ function MoneyRequestReportView({report}: TemporaryMoneyRequestReportViewProps) const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`); - const actionsChatItem = reportActions.filter((ra) => { - return ra.actionName !== 'IOU' && ra.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED; - }); - - // // Get a sorted array of reportActions for both the current report and the transaction thread report associated with this report (if there is one) - // // so that we display transaction-level and report-level report actions in order in the one-transaction view - // const reportActions2 = useMemo( - // () => (reportActionsToDisplay ? getCombinedReportActions(reportActionsToDisplay, transactionThreadReportID ?? null, transactionThreadReportActions ?? []) : []), - // [reportActionsToDisplay, transactionThreadReportActions, transactionThreadReportID], - // ); - - /* Thread's divider line should hide when the first chat in the thread is marked as unread. + /* Todo fix divider line + * Thread's divider line should hide when the first chat in the thread is marked as unread. * This is so that it will not be conflicting with header's separator line. */ // const shouldHideThreadDividerLine = useMemo( @@ -100,45 +95,56 @@ function MoneyRequestReportView({report}: TemporaryMoneyRequestReportViewProps) ); const canPerformWriteAction = canUserPerformWriteAction(report); + + // We are reversing actions because in this View we are starting at the top and don't use Inverted list const visibleReportActions = useMemo(() => { - const filteredActions = actionsChatItem.filter( - (reportAction) => + const filteredActions = reportActions.filter((reportAction) => { + const isChatAction = isChatOnlyReportAction(reportAction); + + return ( + isChatAction && (isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && - shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction), - ); + shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) + ); + }); - return [...filteredActions].toReversed(); - }, [actionsChatItem, isOffline, canPerformWriteAction]); + return filteredActions.toReversed(); + }, [reportActions, isOffline, canPerformWriteAction]); const transactionList = Object.values(transactions).filter((transaction): transaction is Transaction => { return transaction?.reportID === reportID; }); const renderItem = useCallback( - ({item: reportAction, index}: ListRenderItemInfo) => ( - 1} - isFirstVisibleReportAction={false} - /> - ), + ({item: reportAction, index}: ListRenderItemInfo) => { + const displayAsGroup = + !isConsecutiveChronosAutomaticTimerAction(visibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID)) && + hasNextActionMadeBySameActor(visibleReportActions, index); + + return ( + 1} + isFirstVisibleReportAction={false} + /> + ); + }, [ report, + reportActions, visibleReportActions, mostRecentIOUReportActionID, parentReportAction, @@ -147,7 +153,6 @@ function MoneyRequestReportView({report}: TemporaryMoneyRequestReportViewProps) // unreadMarkerReportActionID, // shouldHideThreadDividerLine, // shouldUseThreadDividerLine, - // firstVisibleReportActionID, ], ); @@ -179,8 +184,6 @@ function MoneyRequestReportView({report}: TemporaryMoneyRequestReportViewProps) // onContentSizeChange={onContentSizeChangeInner} // onScroll={trackVerticalScrolling} // onScrollToIndexFailed={onScrollToIndexFailed} - // extraData={extraData} - // key={listID} // shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScrollToTopThreshold} // initialScrollKey={reportActionID} /> diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 9c4ca0f1cebc9..f8fc227591dc0 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -537,12 +537,25 @@ function extractLinksFromMessageHtml(reportAction: OnyxEntry): str * @param reportActions - all actions * @param actionIndex - index of the action */ -function findPreviousAction(reportActions: ReportAction[] | undefined, actionIndex: number): OnyxEntry { - if (!reportActions) { - return undefined; +function findPreviousAction(reportActions: ReportAction[], actionIndex: number): OnyxEntry { + for (let i = actionIndex + 1; i < reportActions.length; i++) { + // Find the next non-pending deletion report action, as the pending delete action means that it is not displayed in the UI, but still is in the report actions list. + // If we are offline, all actions are pending but shown in the UI, so we take the previous action, even if it is a delete. + if (isNetworkOffline || reportActions.at(i)?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { + return reportActions.at(i); + } } - for (let i = actionIndex + 1; i < reportActions.length; i++) { + return undefined; +} + +/** + * Returns the report action immediately after the specified index. + * @param reportActions - all actions + * @param actionIndex - index of the action + */ +function findNextAction(reportActions: ReportAction[], actionIndex: number): OnyxEntry { + for (let i = actionIndex - 1; i > 0; i--) { // Find the next non-pending deletion report action, as the pending delete action means that it is not displayed in the UI, but still is in the report actions list. // If we are offline, all actions are pending but shown in the UI, so we take the previous action, even if it is a delete. if (isNetworkOffline || reportActions.at(i)?.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) { @@ -559,9 +572,9 @@ function findPreviousAction(reportActions: ReportAction[] | undefined, actionInd * * @param actionIndex - index of the comment item in state to check */ -function isConsecutiveActionMadeByPreviousActor(reportActions: ReportAction[] | undefined, actionIndex: number): boolean { +function isConsecutiveActionMadeByPreviousActor(reportActions: ReportAction[], actionIndex: number): boolean { const previousAction = findPreviousAction(reportActions, actionIndex); - const currentAction = reportActions?.[actionIndex]; + const currentAction = reportActions.at(actionIndex); // It's OK for there to be no previous action, and in that case, false will be returned // so that the comment isn't grouped @@ -609,6 +622,60 @@ function isConsecutiveActionMadeByPreviousActor(reportActions: ReportAction[] | return currentAction.actorAccountID === previousAction.actorAccountID; } +// Todo - combine this with `isConsecutiveActionMadeByPreviousActor` so as to not duplicate logic +function hasNextActionMadeBySameActor(reportActions: ReportAction[], actionIndex: number) { + const currentAction = reportActions.at(actionIndex); + const nextAction = findNextAction(reportActions, actionIndex); + + // Todo first should have avatar - verify that this works with long chats + if (actionIndex === 0) { + return false; + } + + // It's OK for there to be no previous action, and in that case, false will be returned + // so that the comment isn't grouped + if (!currentAction || !nextAction) { + return true; + } + + // Comments are only grouped if they happen within 5 minutes of each other + if (new Date(currentAction.created).getTime() - new Date(nextAction.created).getTime() > 300000) { + return false; + } + + // Do not group if previous action was a created action + if (nextAction.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED) { + return false; + } + + // Do not group if previous or current action was a renamed action + if (nextAction.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED || currentAction.actionName === CONST.REPORT.ACTIONS.TYPE.RENAMED) { + return false; + } + + // Do not group if the delegate account ID is different + if (nextAction.delegateAccountID !== currentAction.delegateAccountID) { + return false; + } + + // Do not group if one of previous / current action is report preview and another one is not report preview + if ((isReportPreviewAction(nextAction) && !isReportPreviewAction(currentAction)) || (isReportPreviewAction(currentAction) && !isReportPreviewAction(nextAction))) { + return false; + } + + if (isSubmittedAction(currentAction)) { + const currentActionAdminAccountID = currentAction.adminAccountID; + + return currentActionAdminAccountID === nextAction.actorAccountID || currentActionAdminAccountID === nextAction.adminAccountID; + } + + if (isSubmittedAction(nextAction)) { + return typeof nextAction.adminAccountID === 'number' ? currentAction.actorAccountID === nextAction.adminAccountID : currentAction.actorAccountID === nextAction.actorAccountID; + } + + return currentAction.actorAccountID === nextAction.actorAccountID; +} + function isChronosAutomaticTimerAction(reportAction: OnyxInputOrEntry, isChronosReport: boolean): boolean { const isAutomaticStartTimerAction = () => /start(?:ed|ing)?(?:\snow)?/i.test(getReportActionText(reportAction)); const isAutomaticStopTimerAction = () => /stop(?:ped|ping)?(?:\snow)?/i.test(getReportActionText(reportAction)); @@ -2264,6 +2331,7 @@ export { isClosedAction, isConsecutiveActionMadeByPreviousActor, isConsecutiveChronosAutomaticTimerAction, + hasNextActionMadeBySameActor, isCreatedAction, isCreatedTaskReportAction, isCurrentActionUnread, diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 2af937a59046e..67dfcbb5fd839 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -78,6 +78,7 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) { policy={policy} reportActions={[]} transactionThreadReportID={undefined} + shouldDisplayBackButton onBackButtonPress={() => { Navigation.goBack(); }} @@ -115,6 +116,7 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) { policy={policy} reportActions={[]} transactionThreadReportID={undefined} + shouldDisplayBackButton onBackButtonPress={() => { Navigation.goBack(); }} From dd279c5952f9158998e90178bb8195877cb4aa37 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Mon, 17 Mar 2025 15:04:11 +0100 Subject: [PATCH 4/8] Refactor code for displaying MoneyRequestReportView --- .../MoneyRequestReportActionsList.tsx | 208 +++++++++++++++++ .../MoneyRequestReportTableHeader.tsx | 7 - .../MoneyRequestReportTransactionList.tsx | 30 ++- .../MoneyRequestReportView.tsx | 216 +++++------------- src/hooks/useReportActionsLoading.ts | 128 +++++++++++ .../Search/SearchMoneyRequestReportPage.tsx | 44 +--- src/pages/home/report/ReportActionsList.tsx | 11 - src/pages/home/report/ReportActionsView.tsx | 101 ++------ .../perf-test/ReportActionsList.perf-test.tsx | 1 - 9 files changed, 433 insertions(+), 313 deletions(-) create mode 100644 src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx create mode 100644 src/hooks/useReportActionsLoading.ts diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx new file mode 100644 index 0000000000000..c0a3256fe131c --- /dev/null +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -0,0 +1,208 @@ +import type {ListRenderItemInfo} from '@react-native/virtualized-lists/Lists/VirtualizedList'; +import React, {useCallback, useMemo} from 'react'; +import {InteractionManager, View} from 'react-native'; +import type {OnyxEntry} from 'react-native-onyx'; +import {useOnyx} from 'react-native-onyx'; +import FlatList from '@components/FlatList'; +import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; +import useNetwork from '@hooks/useNetwork'; +import useReportActionsLoading from '@hooks/useReportActionsLoading'; +import useThemeStyles from '@hooks/useThemeStyles'; +import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; +import { + getFirstVisibleReportActionID, + getMostRecentIOURequestActionID, + getOneTransactionThreadReportID, + hasNextActionMadeBySameActor, + isConsecutiveChronosAutomaticTimerAction, + isDeletedParentAction, + isReversedTransaction, + isTransactionThread, + shouldReportActionBeVisible, +} from '@libs/ReportActionsUtils'; +import {canUserPerformWriteAction, chatIncludesChronosWithID, isCanceledTaskReport, isExpenseReport, isInvoiceReport, isIOUReport, isTaskReport} from '@libs/ReportUtils'; +import isSearchTopmostFullScreenRoute from '@navigation/helpers/isSearchTopmostFullScreenRoute'; +import ReportActionsListItemRenderer from '@pages/home/report/ReportActionsListItemRenderer'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type * as OnyxTypes from '@src/types/onyx'; +import type Transaction from '@src/types/onyx/Transaction'; +import MoneyRequestReportTransactionList from './MoneyRequestReportTransactionList'; + +/** + * In this view we are not handling the special single transaction case, we're just handling the report + */ +const EmptyParentReportActionForTransactionThread = undefined; + +type MoneyRequestReportListProps = { + /** The report */ + report: OnyxTypes.Report; + + /** Array of report actions for this report */ + reportActions?: OnyxTypes.ReportAction[]; + + /** If the report has newer actions to load */ + hasNewerActions: boolean; + + /** If the report has older actions to load */ + hasOlderActions: boolean; +}; + +function getParentReportAction(parentReportActions: OnyxEntry, parentReportActionID: string | undefined): OnyxEntry { + if (!parentReportActions || !parentReportActionID) { + return; + } + return parentReportActions[parentReportActionID]; +} + +function isChatOnlyReportAction(action: OnyxTypes.ReportAction) { + return action.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU && action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED; +} + +/** + * TODO make this component have the same functionalities as `ReportActionsList` + * - onLayout + * - onScroll + * - onScrollToIndexFailed + * - shouldEnableAutoScrollToTopThreshold + * - shouldDisplayNewMarker + * - shouldHideThreadDividerLine + */ +function MoneyRequestReportActionsList({report, reportActions = [], hasNewerActions, hasOlderActions}: MoneyRequestReportListProps) { + const styles = useThemeStyles(); + const {isOffline} = useNetwork(); + + const reportID = report?.reportID; + + const [parentReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(report?.parentReportID)}`, { + canEvict: false, + selector: (parentReportActions) => getParentReportAction(parentReportActions, report?.parentReportActionID), + }); + + const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]); + const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], false); + const firstVisibleReportActionID = useMemo(() => getFirstVisibleReportActionID(reportActions, isOffline), [reportActions, isOffline]); + const [transactions = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); + const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`); + + const canPerformWriteAction = canUserPerformWriteAction(report); + + // We are reversing actions because in this View we are starting at the top and don't use Inverted list + const visibleReportActions = useMemo(() => { + const filteredActions = reportActions.filter((reportAction) => { + const isChatAction = isChatOnlyReportAction(reportAction); + + return ( + isChatAction && + (isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && + shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) + ); + }); + + return filteredActions.toReversed(); + }, [reportActions, isOffline, canPerformWriteAction]); + + const transactionList = Object.values(transactions).filter((transaction): transaction is Transaction => { + return transaction?.reportID === reportID; + }); + + const reportActionIDs = useMemo(() => { + return reportActions?.map((action) => action.reportActionID) ?? []; + }, [reportActions]); + + const {loadOlderChats, loadNewerChats} = useReportActionsLoading({ + reportID, + reportActions, + allReportActionIDs: reportActionIDs, + transactionThreadReport, + hasOlderActions, + hasNewerActions, + }); + + const onStartReached = useCallback(() => { + if (!isSearchTopmostFullScreenRoute()) { + loadNewerChats(false); + return; + } + + InteractionManager.runAfterInteractions(() => requestAnimationFrame(() => loadNewerChats(false))); + }, [loadNewerChats]); + + const onEndReached = useCallback(() => { + loadOlderChats(false); + }, [loadOlderChats]); + + const shouldUseThreadDividerLine = useMemo(() => { + const topReport = reportActions.length > 0 ? reportActions.at(reportActions.length - 1) : null; + + if (topReport && topReport.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED) { + return false; + } + + if (isTransactionThread(parentReportAction)) { + return !isDeletedParentAction(parentReportAction) && !isReversedTransaction(parentReportAction); + } + + if (isTaskReport(report)) { + return !isCanceledTaskReport(report, parentReportAction); + } + + return isExpenseReport(report) || isIOUReport(report) || isInvoiceReport(report); + }, [parentReportAction, report, reportActions]); + + const renderItem = useCallback( + ({item: reportAction, index}: ListRenderItemInfo) => { + const displayAsGroup = + !isConsecutiveChronosAutomaticTimerAction(visibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID)) && + hasNextActionMadeBySameActor(visibleReportActions, index); + + return ( + 1} + isFirstVisibleReportAction={firstVisibleReportActionID === reportAction.reportActionID} + /> + ); + }, + [visibleReportActions, reportActions, parentReportAction, report, transactionThreadReport, mostRecentIOUReportActionID, shouldUseThreadDividerLine, firstVisibleReportActionID], + ); + + return ( + + {report ? ( + item.reportActionID} + initialNumToRender={10} + onEndReached={onEndReached} + onEndReachedThreshold={0.75} + onStartReached={onStartReached} + onStartReachedThreshold={0.75} + ListHeaderComponent={} + keyboardShouldPersistTaps="handled" + /> + ) : ( + + )} + + ); +} + +MoneyRequestReportActionsList.displayName = 'MoneyRequestReportActionsList'; + +export default MoneyRequestReportActionsList; diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx index d34c3fb82eb34..2089948f55f8b 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx @@ -2,7 +2,6 @@ import React from 'react'; import {View} from 'react-native'; import type {SearchColumnType, SortOrder} from '@components/Search/types'; import SortableTableHeader from '@components/SelectionList/SortableTableHeader'; -import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; @@ -63,12 +62,6 @@ const shouldShowColumn = () => true; function MoneyRequestReportTableHeader({sortBy, sortOrder, onSortPress, shouldShowSorting}: SearchTableHeaderProps) { const styles = useThemeStyles(); - const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); - const displayNarrowVersion = isMediumScreenWidth || shouldUseNarrowLayout; - - if (displayNarrowVersion) { - return; - } return ( diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx index bfe79a2491303..a1fa22a7863c9 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx @@ -1,34 +1,32 @@ import React from 'react'; import {View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; import TransactionItemRow from '@components/TransactionItemRow'; +import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import type * as OnyxTypes from '@src/types/onyx'; import MoneyRequestReportTableHeader from './MoneyRequestReportTableHeader'; type MoneyRequestReportTransactionListProps = { - /** The report */ - report: OnyxEntry; - /** List of transactions belonging to one report */ transactions: OnyxTypes.Transaction[]; }; -/** - * TODO - * This component is under construction and not yet displayed to any users. - */ -function MoneyRequestReportTransactionList({report, transactions}: MoneyRequestReportTransactionListProps) { +function MoneyRequestReportTransactionList({transactions}: MoneyRequestReportTransactionListProps) { const styles = useThemeStyles(); + const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout(); + + const displayNarrowVersion = isMediumScreenWidth || shouldUseNarrowLayout; return ( <> - {}} - /> + {!displayNarrowVersion && ( + {}} + /> + )} {transactions.map((transaction) => { return ( @@ -37,7 +35,7 @@ function MoneyRequestReportTransactionList({report, transactions}: MoneyRequestR transactionItem={transaction} isSelected={false} shouldShowTooltip - shouldUseNarrowLayout={false} + shouldUseNarrowLayout={displayNarrowVersion} /> ); diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index b48aa73ea6910..0225ffe1ebdc4 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -1,34 +1,29 @@ -import type {ListRenderItemInfo} from '@react-native/virtualized-lists/Lists/VirtualizedList'; -import React, {useCallback, useMemo} from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; -import FlatList from '@components/FlatList'; -import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; +import HeaderGap from '@components/HeaderGap'; +import MoneyReportHeader from '@components/MoneyReportHeader'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; import useThemeStyles from '@hooks/useThemeStyles'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; -import { - getMostRecentIOURequestActionID, - getOneTransactionThreadReportID, - getSortedReportActionsForDisplay, - hasNextActionMadeBySameActor, - isConsecutiveChronosAutomaticTimerAction, - isDeletedParentAction, - shouldReportActionBeVisible, -} from '@libs/ReportActionsUtils'; -import {canUserPerformWriteAction, chatIncludesChronosWithID} from '@libs/ReportUtils'; -import ReportActionsListItemRenderer from '@pages/home/report/ReportActionsListItemRenderer'; -import CONST from '@src/CONST'; +import {isMoneyRequestAction} from '@libs/ReportActionsUtils'; +import {canEditReportAction, getReportOfflinePendingActionAndErrors} from '@libs/ReportUtils'; +import Navigation from '@navigation/Navigation'; +import ReportFooter from '@pages/home/report/ReportFooter'; import ONYXKEYS from '@src/ONYXKEYS'; import type * as OnyxTypes from '@src/types/onyx'; -import type Transaction from '@src/types/onyx/Transaction'; -import {isEmptyObject} from '@src/types/utils/EmptyObject'; -import MoneyRequestReportTransactionList from './MoneyRequestReportTransactionList'; +import MoneyRequestReportActionsList from './MoneyRequestReportActionsList'; -type TemporaryMoneyRequestReportViewProps = { +type MoneyRequestReportViewProps = { /** The report */ report: OnyxEntry; + + /** Metadata for report */ + reportMetadata: OnyxEntry; + + /** Current policy */ + policy: OnyxEntry; }; function getParentReportAction(parentReportActions: OnyxEntry, parentReportActionID: string | undefined): OnyxEntry { @@ -38,158 +33,73 @@ function getParentReportAction(parentReportActions: OnyxEntry {}; -/** - * TODO This component is under construction and not yet displayed to any users. - */ -function MoneyRequestReportView({report}: TemporaryMoneyRequestReportViewProps) { +function MoneyRequestReportView({report, policy, reportMetadata}: MoneyRequestReportViewProps) { const styles = useThemeStyles(); const reportID = report?.reportID; - - const [reportOnyx] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {allowStaleData: true}); - const [parentReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(reportOnyx?.parentReportID)}`, { - canEvict: false, - selector: (parentReportActions) => getParentReportAction(parentReportActions, reportOnyx?.parentReportActionID), - }); + const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, {initialValue: false}); + const {reportPendingAction} = getReportOfflinePendingActionAndErrors(report); const { reportActions, - // linkedAction, Todo - do I need this? + hasNewerActions, + hasOlderActions, // sortedAllReportActions, - // hasNewerActions, - // hasOlderActions } = usePaginatedReportActions(reportID); - const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]); - - const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], false); - const [transactionThreadReportActions] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`, { - selector: (actions: OnyxEntry) => getSortedReportActionsForDisplay(actions, canUserPerformWriteAction(report), true), + const [parentReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(report?.parentReportID)}`, { + canEvict: false, + selector: (parentReportActions) => getParentReportAction(parentReportActions, report?.parentReportActionID), }); - const [transactions = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); - - const isOffline = false; + const lastReportAction = [...reportActions, parentReportAction].find((action) => canEditReportAction(action) && !isMoneyRequestAction(action)); - const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`); - - /* Todo fix divider line - * Thread's divider line should hide when the first chat in the thread is marked as unread. - * This is so that it will not be conflicting with header's separator line. + /** + * When false the ReportActionsView will completely unmount, and we will show a loader until it returns true. */ - // const shouldHideThreadDividerLine = useMemo( - // (): boolean => getFirstVisibleReportActionID(reportActions, isOffline) === unreadMarkerReportActionID, - // [reportActions, isOffline, unreadMarkerReportActionID], - // ); - - const parentReportActionForTransactionThread = useMemo( - () => - isEmptyObject(transactionThreadReportActions) - ? undefined - : (reportActions?.find((action) => action.reportActionID === transactionThreadReport?.parentReportActionID) as OnyxEntry), - [reportActions, transactionThreadReportActions, transactionThreadReport?.parentReportActionID], - ); - - const canPerformWriteAction = canUserPerformWriteAction(report); - - // We are reversing actions because in this View we are starting at the top and don't use Inverted list - const visibleReportActions = useMemo(() => { - const filteredActions = reportActions.filter((reportAction) => { - const isChatAction = isChatOnlyReportAction(reportAction); + const isCurrentReportLoadedFromOnyx = useMemo((): boolean => { + // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely + const isTransitioning = report && report?.reportID !== reportID; + return reportID !== '' && !!report?.reportID && !isTransitioning; + }, [report, reportID]); - return ( - isChatAction && - (isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && - shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) - ); - }); - - return filteredActions.toReversed(); - }, [reportActions, isOffline, canPerformWriteAction]); - - const transactionList = Object.values(transactions).filter((transaction): transaction is Transaction => { - return transaction?.reportID === reportID; - }); - - const renderItem = useCallback( - ({item: reportAction, index}: ListRenderItemInfo) => { - const displayAsGroup = - !isConsecutiveChronosAutomaticTimerAction(visibleReportActions, index, chatIncludesChronosWithID(reportAction?.reportID)) && - hasNextActionMadeBySameActor(visibleReportActions, index); - - return ( - 1} - isFirstVisibleReportAction={false} - /> - ); - }, - [ - report, - reportActions, - visibleReportActions, - mostRecentIOUReportActionID, - parentReportAction, - transactionThreadReport, - parentReportActionForTransactionThread, - // unreadMarkerReportActionID, - // shouldHideThreadDividerLine, - // shouldUseThreadDividerLine, - ], - ); - - const listHeaderComponent = ( - - ); + if (!report) { + return; + } return ( - {report ? ( - item.reportActionID} - initialNumToRender={10} - // onEndReached={onEndReached} - onEndReachedThreshold={0.75} - // onStartReached={onStartReached} - onStartReachedThreshold={0.75} - ListHeaderComponent={listHeaderComponent} - keyboardShouldPersistTaps="handled" - // onLayout={onLayoutInner} - // onContentSizeChange={onContentSizeChangeInner} - // onScroll={trackVerticalScrolling} - // onScrollToIndexFailed={onScrollToIndexFailed} - // shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScrollToTopThreshold} - // initialScrollKey={reportActionID} + + { + Navigation.goBack(); + }} + /> + + {isCurrentReportLoadedFromOnyx ? ( + - ) : ( - - )} + ) : null} ); } diff --git a/src/hooks/useReportActionsLoading.ts b/src/hooks/useReportActionsLoading.ts new file mode 100644 index 0000000000000..f11e763e05b5b --- /dev/null +++ b/src/hooks/useReportActionsLoading.ts @@ -0,0 +1,128 @@ +import {useIsFocused} from '@react-navigation/native'; +import {useCallback, useMemo, useRef} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {getNewerActions, getOlderActions} from '@userActions/Report'; +import CONST from '@src/CONST'; +import type {Report, ReportAction} from '@src/types/onyx'; +import {isEmptyObject} from '@src/types/utils/EmptyObject'; +import useNetwork from './useNetwork'; + +type UseReportActionLoadingArguments = { + /** The id of the current report */ + reportID: string; + + /** The id of the report Action (if specific action was linked to */ + reportActionID?: string; + + /** The id of the current report */ + reportActions: ReportAction[]; + + allReportActionIDs: string[]; + + /** The transaction thread report associated with the current report, if any */ + transactionThreadReport: OnyxEntry; + + /** If the report has newer actions to load */ + hasNewerActions: boolean; + + /** If the report has older actions to load */ + hasOlderActions: boolean; +}; + +/** + * Provides reusable logic to get the functions for loading older/newer report actions. + * Used in the report displaying components + */ +function useReportActionsLoading({reportID, reportActionID, reportActions, allReportActionIDs, transactionThreadReport, hasOlderActions, hasNewerActions}: UseReportActionLoadingArguments) { + const didLoadOlderChats = useRef(false); + const didLoadNewerChats = useRef(false); + + const {isOffline} = useNetwork(); + const isFocused = useIsFocused(); + + const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]); + const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); + + const reportActionIDMap = useMemo(() => { + return reportActions.map((action) => ({ + reportActionID: action.reportActionID, + reportID: allReportActionIDs?.includes(action.reportActionID) ? reportID : transactionThreadReport?.reportID, + })); + }, [reportActions, allReportActionIDs, reportID, transactionThreadReport?.reportID]); + + /** + * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently + * displaying. + */ + const loadOlderChats = useCallback( + (force = false) => { + // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. + if (!force && isOffline) { + return; + } + + // Don't load more chats if we're already at the beginning of the chat history + if (!oldestReportAction || !hasOlderActions) { + return; + } + + didLoadOlderChats.current = true; + + if (!isEmptyObject(transactionThreadReport)) { + // Get older actions based on the oldest reportAction for the current report + const oldestActionCurrentReport = reportActionIDMap.findLast((item) => item.reportID === reportID); + getOlderActions(oldestActionCurrentReport?.reportID, oldestActionCurrentReport?.reportActionID); + + // Get older actions based on the oldest reportAction for the transaction thread report + const oldestActionTransactionThreadReport = reportActionIDMap.findLast((item) => item.reportID === transactionThreadReport.reportID); + getOlderActions(oldestActionTransactionThreadReport?.reportID, oldestActionTransactionThreadReport?.reportActionID); + } else { + // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments + getOlderActions(reportID, oldestReportAction.reportActionID); + } + }, + [isOffline, oldestReportAction, reportID, reportActionIDMap, transactionThreadReport, hasOlderActions], + ); + + const loadNewerChats = useCallback( + (force = false) => { + if ( + !force && + (!reportActionID || + !isFocused || + !newestReportAction || + !hasNewerActions || + isOffline || + // If there was an error only try again once on initial mount. We should also still load + // more in case we have cached messages. + didLoadNewerChats.current || + newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) + ) { + return; + } + + didLoadNewerChats.current = true; + + // If this is a one transaction report, ensure we load newer actions for both this report and the report associated with the transaction + if (!isEmptyObject(transactionThreadReport)) { + // Get newer actions based on the newest reportAction for the current report + const newestActionCurrentReport = reportActionIDMap.find((item) => item.reportID === reportID); + getNewerActions(newestActionCurrentReport?.reportID, newestActionCurrentReport?.reportActionID); + + // Get newer actions based on the newest reportAction for the transaction thread report + const newestActionTransactionThreadReport = reportActionIDMap.find((item) => item.reportID === transactionThreadReport.reportID); + getNewerActions(newestActionTransactionThreadReport?.reportID, newestActionTransactionThreadReport?.reportActionID); + } else if (newestReportAction) { + getNewerActions(reportID, newestReportAction.reportActionID); + } + }, + [reportActionID, isFocused, newestReportAction, hasNewerActions, isOffline, transactionThreadReport, reportActionIDMap, reportID], + ); + + return { + loadOlderChats, + loadNewerChats, + }; +} + +export default useReportActionsLoading; diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 67dfcbb5fd839..4605868029e91 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -1,10 +1,9 @@ import {PortalHost} from '@gorhom/portal'; -import React, {useCallback, useMemo, useState} from 'react'; +import React, {useMemo} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; import HeaderGap from '@components/HeaderGap'; -import MoneyReportHeader from '@components/MoneyReportHeader'; import MoneyRequestReportView from '@components/MoneyRequestReportView/MoneyRequestReportView'; import BottomTabBar from '@components/Navigation/BottomTabBar'; import BOTTOM_TABS from '@components/Navigation/BottomTabBar/BOTTOM_TABS'; @@ -14,12 +13,10 @@ import type {SearchQueryJSON} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import Navigation from '@libs/Navigation/Navigation'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; import {canUserPerformWriteAction} from '@libs/ReportUtils'; import {buildSearchQueryJSON} from '@libs/SearchQueryUtils'; -import ReportFooter from '@pages/home/report/ReportFooter'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import SearchTypeMenu from './SearchTypeMenu'; @@ -45,14 +42,11 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) { const {reportID} = route.params; - const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, {initialValue: false}); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {allowStaleData: true}); const [reportMetadata = defaultReportMetadata] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`); const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {allowStaleData: true, initialValue: {}}); const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; - const [isComposerFocus, setIsComposerFocus] = useState(false); - /** * When false the ReportActionsView will completely unmount and we will show a loader until it returns true. */ @@ -62,9 +56,6 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) { return reportID !== '' && !!report?.reportID && !isTransitioning; }, [report, reportID]); - const onComposerFocus = useCallback(() => setIsComposerFocus(true), []); - const onComposerBlur = useCallback(() => setIsComposerFocus(false), []); - if (shouldUseNarrowLayout) { return ( - { - Navigation.goBack(); - }} /> - ); } @@ -110,30 +95,11 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) { - - { - Navigation.goBack(); - }} /> - - {isCurrentReportLoadedFromOnyx ? ( - - ) : null} diff --git a/src/pages/home/report/ReportActionsList.tsx b/src/pages/home/report/ReportActionsList.tsx index c0decf2546175..8dfaca2d9e82c 100644 --- a/src/pages/home/report/ReportActionsList.tsx +++ b/src/pages/home/report/ReportActionsList.tsx @@ -108,9 +108,6 @@ type ReportActionsListProps = { /** ID of the list */ listID: number; - /** Callback executed on content size change */ - onContentSizeChange: (w: number, h: number) => void; - /** Should enable auto scroll to top threshold */ shouldEnableAutoScrollToTopThreshold?: boolean; }; @@ -153,7 +150,6 @@ function ReportActionsList({ onLayout, isComposerFullSize, listID, - onContentSizeChange, shouldEnableAutoScrollToTopThreshold, parentReportActionForTransactionThread, }: ReportActionsListProps) { @@ -678,12 +674,6 @@ function ReportActionsList({ }, [isScrollToBottomEnabled, onLayout, reportScrollManager], ); - const onContentSizeChangeInner = useCallback( - (w: number, h: number) => { - onContentSizeChange(w, h); - }, - [onContentSizeChange], - ); // eslint-disable-next-line react-compiler/react-compiler const retryLoadNewerChatsError = useCallback(() => { @@ -753,7 +743,6 @@ function ReportActionsList({ ListHeaderComponent={listHeaderComponent} keyboardShouldPersistTaps="handled" onLayout={onLayoutInner} - onContentSizeChange={onContentSizeChangeInner} onScroll={trackVerticalScrolling} onScrollToIndexFailed={onScrollToIndexFailed} extraData={extraData} diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index 96bba61e681ed..d082efccd5453 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -11,9 +11,10 @@ import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; +import useReportActionsLoading from '@hooks/useReportActionsLoading'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; -import {getNewerActions, getOlderActions, updateLoadingInitialReportAction} from '@libs/actions/Report'; +import {updateLoadingInitialReportAction} from '@libs/actions/Report'; import Timing from '@libs/actions/Timing'; import DateUtils from '@libs/DateUtils'; import getIsReportFullyVisible from '@libs/getIsReportFullyVisible'; @@ -91,12 +92,9 @@ function ReportActionsView({ const reportActionID = route?.params?.reportActionID; const prevReportActionID = usePrevious(reportActionID); const didLayout = useRef(false); - const didLoadOlderChats = useRef(false); - const didLoadNewerChats = useRef(false); const {isOffline} = useNetwork(); const {shouldUseNarrowLayout} = useResponsiveLayout(); - const contentListHeight = useRef(0); const isFocused = useIsFocused(); const [isNavigatingToLinkedMessage, setNavigatingToLinkedMessage] = useState(!!reportActionID); const prevShouldUseNarrowLayoutRef = useRef(shouldUseNarrowLayout); @@ -207,21 +205,12 @@ function ReportActionsView({ [reportActions, isOffline, canPerformWriteAction], ); - const reportActionIDMap = useMemo(() => { - const reportActionIDs = allReportActions?.map((action) => action.reportActionID); - return reportActions.map((action) => ({ - reportActionID: action.reportActionID, - reportID: reportActionIDs?.includes(action.reportActionID) ? reportID : transactionThreadReport?.reportID, - })); - }, [allReportActions, reportID, transactionThreadReport, reportActions]); - const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]); const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]); const lastActionCreated = visibleReportActions.at(0)?.created; const isNewestAction = (actionCreated: string | undefined, lastVisibleActionCreated: string | undefined) => actionCreated && lastVisibleActionCreated ? actionCreated >= lastVisibleActionCreated : actionCreated === lastVisibleActionCreated; const hasNewestReportAction = isNewestAction(lastActionCreated, report.lastVisibleActionCreated) || isNewestAction(lastActionCreated, transactionThreadReport?.lastVisibleActionCreated); - const oldestReportAction = useMemo(() => reportActions?.at(-1), [reportActions]); useEffect(() => { // update ref with current state @@ -229,78 +218,19 @@ function ReportActionsView({ // eslint-disable-next-line react-compiler/react-compiler, react-hooks/exhaustive-deps }, [shouldUseNarrowLayout, reportActions, isReportFullyVisible]); - const onContentSizeChange = useCallback((w: number, h: number) => { - contentListHeight.current = h; - }, []); - - /** - * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently - * displaying. - */ - const loadOlderChats = useCallback( - (force = false) => { - // Only fetch more if we are neither already fetching (so that we don't initiate duplicate requests) nor offline. - if (!force && isOffline) { - return; - } - - // Don't load more chats if we're already at the beginning of the chat history - if (!oldestReportAction || !hasOlderActions) { - return; - } - - didLoadOlderChats.current = true; - - if (!isEmptyObject(transactionThreadReport)) { - // Get older actions based on the oldest reportAction for the current report - const oldestActionCurrentReport = reportActionIDMap.findLast((item) => item.reportID === reportID); - getOlderActions(oldestActionCurrentReport?.reportID, oldestActionCurrentReport?.reportActionID); - - // Get older actions based on the oldest reportAction for the transaction thread report - const oldestActionTransactionThreadReport = reportActionIDMap.findLast((item) => item.reportID === transactionThreadReport.reportID); - getOlderActions(oldestActionTransactionThreadReport?.reportID, oldestActionTransactionThreadReport?.reportActionID); - } else { - // Retrieve the next REPORT.ACTIONS.LIMIT sized page of comments - getOlderActions(reportID, oldestReportAction.reportActionID); - } - }, - [isOffline, oldestReportAction, reportID, reportActionIDMap, transactionThreadReport, hasOlderActions], - ); - - const loadNewerChats = useCallback( - (force = false) => { - if ( - !force && - (!reportActionID || - !isFocused || - !newestReportAction || - !hasNewerActions || - isOffline || - // If there was an error only try again once on initial mount. We should also still load - // more in case we have cached messages. - didLoadNewerChats.current || - newestReportAction.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE) - ) { - return; - } - - didLoadNewerChats.current = true; - - // If this is a one transaction report, ensure we load newer actions for both this report and the report associated with the transaction - if (!isEmptyObject(transactionThreadReport)) { - // Get newer actions based on the newest reportAction for the current report - const newestActionCurrentReport = reportActionIDMap.find((item) => item.reportID === reportID); - getNewerActions(newestActionCurrentReport?.reportID, newestActionCurrentReport?.reportActionID); - - // Get newer actions based on the newest reportAction for the transaction thread report - const newestActionTransactionThreadReport = reportActionIDMap.find((item) => item.reportID === transactionThreadReport.reportID); - getNewerActions(newestActionTransactionThreadReport?.reportID, newestActionTransactionThreadReport?.reportActionID); - } else if (newestReportAction) { - getNewerActions(reportID, newestReportAction.reportActionID); - } - }, - [reportActionID, isFocused, newestReportAction, hasNewerActions, isOffline, transactionThreadReport, reportActionIDMap, reportID], - ); + const allReportActionIDs = useMemo(() => { + return allReportActions?.map((action) => action.reportActionID) ?? []; + }, [allReportActions]); + + const {loadOlderChats, loadNewerChats} = useReportActionsLoading({ + reportID, + reportActionID, + reportActions, + allReportActionIDs, + transactionThreadReport, + hasOlderActions, + hasNewerActions, + }); /** * Runs when the FlatList finishes laying out @@ -380,7 +310,6 @@ function ReportActionsView({ loadOlderChats={loadOlderChats} loadNewerChats={loadNewerChats} listID={listID} - onContentSizeChange={onContentSizeChange} shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScroll} /> diff --git a/tests/perf-test/ReportActionsList.perf-test.tsx b/tests/perf-test/ReportActionsList.perf-test.tsx index dff140a3e63a4..2dd7ed401fc20 100644 --- a/tests/perf-test/ReportActionsList.perf-test.tsx +++ b/tests/perf-test/ReportActionsList.perf-test.tsx @@ -117,7 +117,6 @@ function ReportActionsListWrapper() { report={report} onLayout={mockOnLayout} onScroll={mockOnScroll} - onContentSizeChange={() => {}} listID={1} loadOlderChats={mockLoadChats} loadNewerChats={mockLoadChats} From 9803b3f4c1c2f439d79bbb14be8c02ad0336e532 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Mon, 17 Mar 2025 15:32:04 +0100 Subject: [PATCH 5/8] Add fetching report --- src/pages/Search/SearchMoneyRequestReportPage.tsx | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index 4605868029e91..f98e1a4a109f1 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -1,5 +1,5 @@ import {PortalHost} from '@gorhom/portal'; -import React, {useMemo} from 'react'; +import React, {useEffect, useMemo} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; @@ -17,6 +17,7 @@ import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavig import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; import {canUserPerformWriteAction} from '@libs/ReportUtils'; import {buildSearchQueryJSON} from '@libs/SearchQueryUtils'; +import {openReport} from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; import type SCREENS from '@src/SCREENS'; import SearchTypeMenu from './SearchTypeMenu'; @@ -56,6 +57,10 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) { return reportID !== '' && !!report?.reportID && !isTransitioning; }, [report, reportID]); + useEffect(() => { + openReport(reportID); + }, [reportID]); + if (shouldUseNarrowLayout) { return ( Date: Tue, 18 Mar 2025 11:24:22 +0100 Subject: [PATCH 6/8] Fix RBR positioning on mobile screen --- .../TransactionItemRow/TransactionItemRowRBR.tsx | 5 +++-- src/components/TransactionItemRow/index.tsx | 8 ++++++-- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx index ecd11626faad2..e9c1629e87d4e 100644 --- a/src/components/TransactionItemRow/TransactionItemRowRBR.tsx +++ b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx @@ -1,4 +1,5 @@ import React from 'react'; +import type {ViewStyle} from 'react-native'; import {View} from 'react-native'; import Icon from '@components/Icon'; import {DotIndicator} from '@components/Icon/Expensicons'; @@ -11,7 +12,7 @@ import ViolationsUtils from '@libs/Violations/ViolationsUtils'; import variables from '@styles/variables'; import type Transaction from '@src/types/onyx/Transaction'; -function TransactionItemRowRBR({transaction}: {transaction: Transaction}) { +function TransactionItemRowRBR({transaction, containerStyles}: {transaction: Transaction; containerStyles?: ViewStyle[]}) { const styles = useThemeStyles(); const transactionViolations = useTransactionViolations(transaction?.transactionID); const {translate} = useLocalize(); @@ -26,7 +27,7 @@ function TransactionItemRowRBR({transaction}: {transaction: Transaction}) { return ( transactionViolations.length > 0 && ( - + @@ -72,7 +73,7 @@ function TransactionItemRow({ - + - + )} From 94df3116cf0efca1ff06538fecb67b571e5b2a25 Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Tue, 18 Mar 2025 11:47:04 +0100 Subject: [PATCH 7/8] Fix naming of useLoadReportActions and types in SearchTableHeader --- .../MoneyRequestReportActionsList.tsx | 4 ++-- .../MoneyRequestReportTableHeader.tsx | 5 +++-- .../SelectionList/SearchTableHeader.tsx | 11 +++++------ .../SelectionList/SortableTableHeader.tsx | 5 ++--- src/components/SelectionList/types.ts | 4 ++++ ...ionsLoading.ts => useLoadReportActions.ts} | 19 ++++++++++--------- src/pages/home/report/ReportActionsView.tsx | 4 ++-- 7 files changed, 28 insertions(+), 24 deletions(-) rename src/hooks/{useReportActionsLoading.ts => useLoadReportActions.ts} (87%) diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index c0a3256fe131c..54b3c81747350 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -5,8 +5,8 @@ import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import FlatList from '@components/FlatList'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; +import useLoadReportActions from '@hooks/useLoadReportActions'; import useNetwork from '@hooks/useNetwork'; -import useReportActionsLoading from '@hooks/useReportActionsLoading'; import useThemeStyles from '@hooks/useThemeStyles'; import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID'; import { @@ -110,7 +110,7 @@ function MoneyRequestReportActionsList({report, reportActions = [], hasNewerActi return reportActions?.map((action) => action.reportActionID) ?? []; }, [reportActions]); - const {loadOlderChats, loadNewerChats} = useReportActionsLoading({ + const {loadOlderChats, loadNewerChats} = useLoadReportActions({ reportID, reportActions, allReportActionIDs: reportActionIDs, diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx index 2089948f55f8b..af970a2b92f5f 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx @@ -2,12 +2,13 @@ import React from 'react'; import {View} from 'react-native'; import type {SearchColumnType, SortOrder} from '@components/Search/types'; import SortableTableHeader from '@components/SelectionList/SortableTableHeader'; +import type {SortableColumnName} from '@components/SelectionList/types'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; type ColumnConfig = { - columnName: SearchColumnType | typeof CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS; + columnName: SortableColumnName; translationKey: TranslationPaths | undefined; isColumnSortable?: boolean; }; @@ -53,7 +54,7 @@ const columnConfig: ColumnConfig[] = [ type SearchTableHeaderProps = { sortBy?: SearchColumnType; sortOrder?: SortOrder; - onSortPress: (column: SearchColumnType | typeof CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS, order: SortOrder) => void; + onSortPress: (column: SortableColumnName, order: SortOrder) => void; shouldShowSorting: boolean; }; diff --git a/src/components/SelectionList/SearchTableHeader.tsx b/src/components/SelectionList/SearchTableHeader.tsx index dc18c8e17d76b..69a380a3f3d87 100644 --- a/src/components/SelectionList/SearchTableHeader.tsx +++ b/src/components/SelectionList/SearchTableHeader.tsx @@ -1,5 +1,6 @@ import React, {useCallback} from 'react'; import type {SearchColumnType, SortOrder} from '@components/Search/types'; +import type {SortableColumnName} from '@components/SelectionList/types'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import {getShouldShowMerchant} from '@libs/SearchUIUtils'; import CONST from '@src/CONST'; @@ -15,7 +16,7 @@ type SearchColumnConfig = { isColumnSortable?: boolean; }; -const shouldShowColumnConfig: Record = { +const shouldShowColumnConfig: Record = { [CONST.SEARCH.TABLE_COLUMNS.RECEIPT]: () => true, [CONST.SEARCH.TABLE_COLUMNS.TYPE]: () => true, [CONST.SEARCH.TABLE_COLUMNS.DATE]: () => true, @@ -28,6 +29,8 @@ const shouldShowColumnConfig: Record [CONST.SEARCH.TABLE_COLUMNS.TAX_AMOUNT]: (data, metadata) => metadata?.columnsToShow?.shouldShowTaxColumn ?? false, [CONST.SEARCH.TABLE_COLUMNS.TOTAL_AMOUNT]: () => true, [CONST.SEARCH.TABLE_COLUMNS.ACTION]: () => true, + // This column is never displayed on Search + [CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS]: () => false, }; const expenseHeaders: SearchColumnConfig[] = [ @@ -108,11 +111,7 @@ function SearchTableHeader({data, metadata, sortBy, sortOrder, onSortPress, shou const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth; const shouldShowColumn = useCallback( - (columnName: SearchColumnType | typeof CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS) => { - if (columnName === CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS) { - return false; - } - + (columnName: SortableColumnName) => { const shouldShowFun = shouldShowColumnConfig[columnName]; return shouldShowFun(data, metadata); }, diff --git a/src/components/SelectionList/SortableTableHeader.tsx b/src/components/SelectionList/SortableTableHeader.tsx index b16ea1c4c0f35..bd11b11e52e64 100644 --- a/src/components/SelectionList/SortableTableHeader.tsx +++ b/src/components/SelectionList/SortableTableHeader.tsx @@ -1,14 +1,13 @@ import React from 'react'; import {View} from 'react-native'; -import type {SearchColumnType, SortOrder} from '@components/Search/types'; +import type {SortOrder} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; import useThemeStyles from '@hooks/useThemeStyles'; import CONST from '@src/CONST'; import type {TranslationPaths} from '@src/languages/types'; import SortableHeaderText from './SortableHeaderText'; - -type SortableColumnName = SearchColumnType | typeof CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS; +import type {SortableColumnName} from './types'; type ColumnConfig = { columnName: SortableColumnName; diff --git a/src/components/SelectionList/types.ts b/src/components/SelectionList/types.ts index c409c6a61d38d..82226fe1bc517 100644 --- a/src/components/SelectionList/types.ts +++ b/src/components/SelectionList/types.ts @@ -14,6 +14,7 @@ import type { } from 'react-native'; import type {AnimatedStyle} from 'react-native-reanimated'; import type {SearchRouterItem} from '@components/Search/SearchAutocompleteList'; +import type {SearchColumnType} from '@components/Search/types'; import type {BrickRoad} from '@libs/WorkspacesSettingsUtils'; // eslint-disable-next-line no-restricted-imports import type CursorStyles from '@styles/utils/cursor/types'; @@ -705,6 +706,8 @@ type ExtendedSectionListData = ExtendedSectionListData>; +type SortableColumnName = SearchColumnType | typeof CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS; + export type { BaseListItemProps, SelectionListProps, @@ -732,4 +735,5 @@ export type { ValidListItem, ReportActionListItemType, ChatListItemProps, + SortableColumnName, }; diff --git a/src/hooks/useReportActionsLoading.ts b/src/hooks/useLoadReportActions.ts similarity index 87% rename from src/hooks/useReportActionsLoading.ts rename to src/hooks/useLoadReportActions.ts index f11e763e05b5b..e4af06fd3332e 100644 --- a/src/hooks/useReportActionsLoading.ts +++ b/src/hooks/useLoadReportActions.ts @@ -7,19 +7,20 @@ import type {Report, ReportAction} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import useNetwork from './useNetwork'; -type UseReportActionLoadingArguments = { +type UseLoadReportActionsArguments = { /** The id of the current report */ reportID: string; - /** The id of the report Action (if specific action was linked to */ + /** The id of the reportAction (if specific action was linked to */ reportActionID?: string; - /** The id of the current report */ + /** The list of reportActions linked to the current report */ reportActions: ReportAction[]; + /** The IDs of all reportActions linked to the current report (may contain some extra actions) */ allReportActionIDs: string[]; - /** The transaction thread report associated with the current report, if any */ + /** The transaction thread report associated with the current transaction, if any */ transactionThreadReport: OnyxEntry; /** If the report has newer actions to load */ @@ -30,10 +31,10 @@ type UseReportActionLoadingArguments = { }; /** - * Provides reusable logic to get the functions for loading older/newer report actions. + * Provides reusable logic to get the functions for loading older/newer reportActions. * Used in the report displaying components */ -function useReportActionsLoading({reportID, reportActionID, reportActions, allReportActionIDs, transactionThreadReport, hasOlderActions, hasNewerActions}: UseReportActionLoadingArguments) { +function useLoadReportActions({reportID, reportActionID, reportActions, allReportActionIDs, transactionThreadReport, hasOlderActions, hasNewerActions}: UseLoadReportActionsArguments) { const didLoadOlderChats = useRef(false); const didLoadNewerChats = useRef(false); @@ -51,7 +52,7 @@ function useReportActionsLoading({reportID, reportActionID, reportActions, allRe }, [reportActions, allReportActionIDs, reportID, transactionThreadReport?.reportID]); /** - * Retrieves the next set of report actions for the chat once we are nearing the end of what we are currently + * Retrieves the next set of reportActions for the chat once we are nearing the end of what we are currently * displaying. */ const loadOlderChats = useCallback( @@ -61,7 +62,7 @@ function useReportActionsLoading({reportID, reportActionID, reportActions, allRe return; } - // Don't load more chats if we're already at the beginning of the chat history + // Don't load more reportActions if we're already at the beginning of the chat history if (!oldestReportAction || !hasOlderActions) { return; } @@ -125,4 +126,4 @@ function useReportActionsLoading({reportID, reportActionID, reportActions, allRe }; } -export default useReportActionsLoading; +export default useLoadReportActions; diff --git a/src/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx index d082efccd5453..227c69182a366 100755 --- a/src/pages/home/report/ReportActionsView.tsx +++ b/src/pages/home/report/ReportActionsView.tsx @@ -8,10 +8,10 @@ import * as Illustrations from '@components/Icon/Illustrations'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; import SearchRowSkeleton from '@components/Skeletons/SearchRowSkeleton'; import useCopySelectionHelper from '@hooks/useCopySelectionHelper'; +import useLoadReportActions from '@hooks/useLoadReportActions'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePrevious from '@hooks/usePrevious'; -import useReportActionsLoading from '@hooks/useReportActionsLoading'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import {updateLoadingInitialReportAction} from '@libs/actions/Report'; @@ -222,7 +222,7 @@ function ReportActionsView({ return allReportActions?.map((action) => action.reportActionID) ?? []; }, [allReportActions]); - const {loadOlderChats, loadNewerChats} = useReportActionsLoading({ + const {loadOlderChats, loadNewerChats} = useLoadReportActions({ reportID, reportActionID, reportActions, From 8a5121b671d1d8d0e0443f631aed578922fc86da Mon Sep 17 00:00:00 2001 From: Mateusz Titz Date: Tue, 18 Mar 2025 14:28:12 +0100 Subject: [PATCH 8/8] Fix SortableTable column alignment and reuse MoneyRequestReportView code --- .../MoneyRequestReportActionsList.tsx | 18 ++++++++------ .../MoneyRequestReportView.tsx | 18 +++++--------- .../SelectionList/SearchTableHeader.tsx | 5 +++- .../SelectionList/SortableTableHeader.tsx | 6 +++-- src/hooks/useIsReportReadyToDisplay.ts | 24 +++++++++++++++++++ src/libs/ReportActionsUtils.ts | 4 ++-- .../Search/SearchMoneyRequestReportPage.tsx | 17 +++++-------- src/pages/home/ReportScreen.tsx | 13 +++------- 8 files changed, 60 insertions(+), 45 deletions(-) create mode 100644 src/hooks/useIsReportReadyToDisplay.ts diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 54b3c81747350..16aa6244c6aa0 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -1,7 +1,7 @@ import type {ListRenderItemInfo} from '@react-native/virtualized-lists/Lists/VirtualizedList'; import React, {useCallback, useMemo} from 'react'; import {InteractionManager, View} from 'react-native'; -import type {OnyxEntry} from 'react-native-onyx'; +import type {OnyxCollection, OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; import FlatList from '@components/FlatList'; import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView'; @@ -59,6 +59,12 @@ function isChatOnlyReportAction(action: OnyxTypes.ReportAction) { return action.actionName !== CONST.REPORT.ACTIONS.TYPE.IOU && action.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED; } +function getTransactionsForReportID(transactions: OnyxCollection, reportID: string) { + return Object.values(transactions ?? {}).filter((transaction): transaction is Transaction => { + return transaction?.reportID === reportID; + }); +} + /** * TODO make this component have the same functionalities as `ReportActionsList` * - onLayout @@ -82,7 +88,9 @@ function MoneyRequestReportActionsList({report, reportActions = [], hasNewerActi const mostRecentIOUReportActionID = useMemo(() => getMostRecentIOURequestActionID(reportActions), [reportActions]); const transactionThreadReportID = getOneTransactionThreadReportID(reportID, reportActions ?? [], false); const firstVisibleReportActionID = useMemo(() => getFirstVisibleReportActionID(reportActions, isOffline), [reportActions, isOffline]); - const [transactions = {}] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION); + const [transactions = []] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION, { + selector: (allTransactions): OnyxTypes.Transaction[] => getTransactionsForReportID(allTransactions, reportID), + }); const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID ?? CONST.DEFAULT_NUMBER_ID}`); const canPerformWriteAction = canUserPerformWriteAction(report); @@ -102,10 +110,6 @@ function MoneyRequestReportActionsList({report, reportActions = [], hasNewerActi return filteredActions.toReversed(); }, [reportActions, isOffline, canPerformWriteAction]); - const transactionList = Object.values(transactions).filter((transaction): transaction is Transaction => { - return transaction?.reportID === reportID; - }); - const reportActionIDs = useMemo(() => { return reportActions?.map((action) => action.reportActionID) ?? []; }, [reportActions]); @@ -193,7 +197,7 @@ function MoneyRequestReportActionsList({report, reportActions = [], hasNewerActi onEndReachedThreshold={0.75} onStartReached={onStartReached} onStartReachedThreshold={0.75} - ListHeaderComponent={} + ListHeaderComponent={} keyboardShouldPersistTaps="handled" /> ) : ( diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx index 0225ffe1ebdc4..dcf75ce9ea1d3 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx @@ -1,4 +1,4 @@ -import React, {useMemo} from 'react'; +import React from 'react'; import {View} from 'react-native'; import type {OnyxEntry} from 'react-native-onyx'; import {useOnyx} from 'react-native-onyx'; @@ -24,6 +24,9 @@ type MoneyRequestReportViewProps = { /** Current policy */ policy: OnyxEntry; + + /** Whether Report footer (that includes Composer) should be displayed */ + shouldDisplayReportFooter: boolean; }; function getParentReportAction(parentReportActions: OnyxEntry, parentReportActionID: string | undefined): OnyxEntry { @@ -35,7 +38,7 @@ function getParentReportAction(parentReportActions: OnyxEntry {}; -function MoneyRequestReportView({report, policy, reportMetadata}: MoneyRequestReportViewProps) { +function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayReportFooter}: MoneyRequestReportViewProps) { const styles = useThemeStyles(); const reportID = report?.reportID; @@ -56,15 +59,6 @@ function MoneyRequestReportView({report, policy, reportMetadata}: MoneyRequestRe const lastReportAction = [...reportActions, parentReportAction].find((action) => canEditReportAction(action) && !isMoneyRequestAction(action)); - /** - * When false the ReportActionsView will completely unmount, and we will show a loader until it returns true. - */ - const isCurrentReportLoadedFromOnyx = useMemo((): boolean => { - // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely - const isTransitioning = report && report?.reportID !== reportID; - return reportID !== '' && !!report?.reportID && !isTransitioning; - }, [report, reportID]); - if (!report) { return; } @@ -88,7 +82,7 @@ function MoneyRequestReportView({report, policy, reportMetadata}: MoneyRequestRe hasOlderActions={hasOlderActions} hasNewerActions={hasNewerActions} /> - {isCurrentReportLoadedFromOnyx ? ( + {shouldDisplayReportFooter ? ( boolean; @@ -106,6 +107,7 @@ type SearchTableHeaderProps = { }; function SearchTableHeader({data, metadata, sortBy, sortOrder, onSortPress, shouldShowYear, shouldShowSorting}: SearchTableHeaderProps) { + const styles = useThemeStyles(); // eslint-disable-next-line rulesdir/prefer-shouldUseNarrowLayout-instead-of-isSmallScreenWidth const {isSmallScreenWidth, isMediumScreenWidth} = useResponsiveLayout(); const displayNarrowVersion = isMediumScreenWidth || isSmallScreenWidth; @@ -136,6 +138,7 @@ function SearchTableHeader({data, metadata, sortBy, sortOrder, onSortPress, shou shouldShowSorting={shouldShowSorting} sortBy={sortBy} sortOrder={sortOrder} + containerStyles={styles.pl4} onSortPress={(columnName, order) => { if (columnName === CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS) { return; diff --git a/src/components/SelectionList/SortableTableHeader.tsx b/src/components/SelectionList/SortableTableHeader.tsx index bd11b11e52e64..ff6009bb12248 100644 --- a/src/components/SelectionList/SortableTableHeader.tsx +++ b/src/components/SelectionList/SortableTableHeader.tsx @@ -1,5 +1,6 @@ import React from 'react'; import {View} from 'react-native'; +import type {StyleProp, ViewStyle} from 'react-native'; import type {SortOrder} from '@components/Search/types'; import useLocalize from '@hooks/useLocalize'; import useStyleUtils from '@hooks/useStyleUtils'; @@ -21,18 +22,19 @@ type SearchTableHeaderProps = { sortOrder?: SortOrder; shouldShowSorting: boolean; dateColumnSize: 'normal' | 'wide'; + containerStyles?: StyleProp; shouldShowColumn: (columnName: SortableColumnName) => boolean; onSortPress: (column: SortableColumnName, order: SortOrder) => void; }; -function SortableTableHeader({columns, sortBy, sortOrder, onSortPress, shouldShowColumn, dateColumnSize, shouldShowSorting}: SearchTableHeaderProps) { +function SortableTableHeader({columns, sortBy, sortOrder, shouldShowColumn, dateColumnSize, containerStyles, shouldShowSorting, onSortPress}: SearchTableHeaderProps) { const styles = useThemeStyles(); const StyleUtils = useStyleUtils(); const {translate} = useLocalize(); return ( - + {columns.map(({columnName, translationKey, isColumnSortable}) => { if (!shouldShowColumn(columnName)) { return null; diff --git a/src/hooks/useIsReportReadyToDisplay.ts b/src/hooks/useIsReportReadyToDisplay.ts new file mode 100644 index 0000000000000..15cdc5190bb35 --- /dev/null +++ b/src/hooks/useIsReportReadyToDisplay.ts @@ -0,0 +1,24 @@ +import {useMemo} from 'react'; +import type {OnyxEntry} from 'react-native-onyx'; +import {canUserPerformWriteAction} from '@libs/ReportUtils'; +import type {Report} from '@src/types/onyx'; + +function useIsReportReadyToDisplay(report: OnyxEntry, reportIDFromRoute: string | undefined) { + /** + * When false the report is not ready to be fully displayed + */ + const isCurrentReportLoadedFromOnyx = useMemo((): boolean => { + // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely + const isTransitioning = report && report?.reportID !== reportIDFromRoute; + return reportIDFromRoute !== '' && !!report?.reportID && !isTransitioning; + }, [report, reportIDFromRoute]); + + const isEditingDisabled = useMemo(() => !isCurrentReportLoadedFromOnyx || !canUserPerformWriteAction(report), [isCurrentReportLoadedFromOnyx, report]); + + return { + isCurrentReportLoadedFromOnyx, + isEditingDisabled, + }; +} + +export default useIsReportReadyToDisplay; diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index f8fc227591dc0..c2561943d2f56 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -622,12 +622,12 @@ function isConsecutiveActionMadeByPreviousActor(reportActions: ReportAction[], a return currentAction.actorAccountID === previousAction.actorAccountID; } -// Todo - combine this with `isConsecutiveActionMadeByPreviousActor` so as to not duplicate logic +// Todo combine with `isConsecutiveActionMadeByPreviousActor` so as to not duplicate logic (issue: https://github.com/Expensify/App/issues/58625) function hasNextActionMadeBySameActor(reportActions: ReportAction[], actionIndex: number) { const currentAction = reportActions.at(actionIndex); const nextAction = findNextAction(reportActions, actionIndex); - // Todo first should have avatar - verify that this works with long chats + // Todo first should have avatar - verify that this works with long chats (issue: https://github.com/Expensify/App/issues/58625) if (actionIndex === 0) { return false; } diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx index f98e1a4a109f1..57ff2a38dcff0 100644 --- a/src/pages/Search/SearchMoneyRequestReportPage.tsx +++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx @@ -1,5 +1,5 @@ import {PortalHost} from '@gorhom/portal'; -import React, {useEffect, useMemo} from 'react'; +import React, {useEffect} from 'react'; import {View} from 'react-native'; import {useOnyx} from 'react-native-onyx'; import DragAndDropProvider from '@components/DragAndDrop/Provider'; @@ -10,12 +10,12 @@ import BOTTOM_TABS from '@components/Navigation/BottomTabBar/BOTTOM_TABS'; import TopBar from '@components/Navigation/TopBar'; import ScreenWrapper from '@components/ScreenWrapper'; import type {SearchQueryJSON} from '@components/Search/types'; +import useIsReportReadyToDisplay from '@hooks/useIsReportReadyToDisplay'; import useLocalize from '@hooks/useLocalize'; import useResponsiveLayout from '@hooks/useResponsiveLayout'; import useThemeStyles from '@hooks/useThemeStyles'; import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types'; import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types'; -import {canUserPerformWriteAction} from '@libs/ReportUtils'; import {buildSearchQueryJSON} from '@libs/SearchQueryUtils'; import {openReport} from '@userActions/Report'; import ONYXKEYS from '@src/ONYXKEYS'; @@ -48,14 +48,7 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) { const [policies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {allowStaleData: true, initialValue: {}}); const policy = policies?.[`${ONYXKEYS.COLLECTION.POLICY}${report?.policyID}`]; - /** - * When false the ReportActionsView will completely unmount and we will show a loader until it returns true. - */ - const isCurrentReportLoadedFromOnyx = useMemo((): boolean => { - // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely - const isTransitioning = report && report?.reportID !== reportID; - return reportID !== '' && !!report?.reportID && !isTransitioning; - }, [report, reportID]); + const {isEditingDisabled, isCurrentReportLoadedFromOnyx} = useIsReportReadyToDisplay(report, reportID); useEffect(() => { openReport(reportID); @@ -73,6 +66,7 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) { report={report} reportMetadata={reportMetadata} policy={policy} + shouldDisplayReportFooter={isCurrentReportLoadedFromOnyx} /> ); @@ -98,12 +92,13 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) { - + diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx index fd3fe04f70f1d..1e859776d93f1 100644 --- a/src/pages/home/ReportScreen.tsx +++ b/src/pages/home/ReportScreen.tsx @@ -20,6 +20,7 @@ import useAppFocusEvent from '@hooks/useAppFocusEvent'; import type {CurrentReportIDContextValue} from '@hooks/useCurrentReportID'; import useCurrentReportID from '@hooks/useCurrentReportID'; import useDeepCompareRef from '@hooks/useDeepCompareRef'; +import useIsReportReadyToDisplay from '@hooks/useIsReportReadyToDisplay'; import useLocalize from '@hooks/useLocalize'; import useNetwork from '@hooks/useNetwork'; import usePaginatedReportActions from '@hooks/usePaginatedReportActions'; @@ -358,14 +359,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { ); } - /** - * When false the ReportActionsView will completely unmount and we will show a loader until it returns true. - */ - const isCurrentReportLoadedFromOnyx = useMemo((): boolean => { - // This is necessary so that when we are retrieving the next report data from Onyx the ReportActionsView will remount completely - const isTransitioning = report && report?.reportID !== reportIDFromRoute; - return reportIDFromRoute !== '' && !!report?.reportID && !isTransitioning; - }, [report, reportIDFromRoute]); + const {isEditingDisabled, isCurrentReportLoadedFromOnyx} = useIsReportReadyToDisplay(report, reportIDFromRoute); const isLinkedActionDeleted = useMemo( () => !!linkedAction && !shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, canUserPerformWriteAction(report)), @@ -722,7 +716,7 @@ function ReportScreen({route, navigation}: ReportScreenProps) { shouldShowButton /> )} - + )} - {isCurrentReportLoadedFromOnyx ? (