diff --git a/src/CONST.ts b/src/CONST.ts
index 745236dc52ec..8fd13e858b35 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/MoneyReportHeader.tsx b/src/components/MoneyReportHeader.tsx
index a085385589a3..43f64670ca99 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 (
, 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;
+}
+
+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
+ * - 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, {
+ selector: (allTransactions): OnyxTypes.Transaction[] => getTransactionsForReportID(allTransactions, reportID),
+ });
+ 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 reportActionIDs = useMemo(() => {
+ return reportActions?.map((action) => action.reportActionID) ?? [];
+ }, [reportActions]);
+
+ const {loadOlderChats, loadNewerChats} = useLoadReportActions({
+ 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
new file mode 100644
index 000000000000..af970a2b92f5
--- /dev/null
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTableHeader.tsx
@@ -0,0 +1,84 @@
+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: SortableColumnName;
+ 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: SortableColumnName, 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();
+
+ 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 000000000000..a1fa22a7863c
--- /dev/null
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportTransactionList.tsx
@@ -0,0 +1,50 @@
+import React from 'react';
+import {View} from 'react-native';
+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 = {
+ /** List of transactions belonging to one report */
+ transactions: OnyxTypes.Transaction[];
+};
+
+function MoneyRequestReportTransactionList({transactions}: MoneyRequestReportTransactionListProps) {
+ const styles = useThemeStyles();
+ const {shouldUseNarrowLayout, isMediumScreenWidth} = useResponsiveLayout();
+
+ const displayNarrowVersion = isMediumScreenWidth || shouldUseNarrowLayout;
+
+ return (
+ <>
+ {!displayNarrowVersion && (
+ {}}
+ />
+ )}
+
+ {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 000000000000..dcf75ce9ea1d
--- /dev/null
+++ b/src/components/MoneyRequestReportView/MoneyRequestReportView.tsx
@@ -0,0 +1,103 @@
+import React from 'react';
+import {View} from 'react-native';
+import type {OnyxEntry} from 'react-native-onyx';
+import {useOnyx} from 'react-native-onyx';
+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 {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 MoneyRequestReportActionsList from './MoneyRequestReportActionsList';
+
+type MoneyRequestReportViewProps = {
+ /** The report */
+ report: OnyxEntry;
+
+ /** Metadata for report */
+ reportMetadata: OnyxEntry;
+
+ /** Current policy */
+ policy: OnyxEntry;
+
+ /** Whether Report footer (that includes Composer) should be displayed */
+ shouldDisplayReportFooter: boolean;
+};
+
+function getParentReportAction(parentReportActions: OnyxEntry, parentReportActionID: string | undefined): OnyxEntry {
+ if (!parentReportActions || !parentReportActionID) {
+ return;
+ }
+ return parentReportActions[parentReportActionID];
+}
+
+const noOp = () => {};
+
+function MoneyRequestReportView({report, policy, reportMetadata, shouldDisplayReportFooter}: MoneyRequestReportViewProps) {
+ const styles = useThemeStyles();
+
+ const reportID = report?.reportID;
+ const [isComposerFullSize] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_IS_COMPOSER_FULL_SIZE}${reportID}`, {initialValue: false});
+ const {reportPendingAction} = getReportOfflinePendingActionAndErrors(report);
+
+ const {
+ reportActions,
+ hasNewerActions,
+ hasOlderActions,
+ // sortedAllReportActions,
+ } = usePaginatedReportActions(reportID);
+
+ const [parentReportAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${getNonEmptyStringOnyxID(report?.parentReportID)}`, {
+ canEvict: false,
+ selector: (parentReportActions) => getParentReportAction(parentReportActions, report?.parentReportActionID),
+ });
+
+ const lastReportAction = [...reportActions, parentReportAction].find((action) => canEditReportAction(action) && !isMoneyRequestAction(action));
+
+ if (!report) {
+ return;
+ }
+
+ return (
+
+
+ {
+ Navigation.goBack();
+ }}
+ />
+
+ {shouldDisplayReportFooter ? (
+
+ ) : null}
+
+ );
+}
+
+MoneyRequestReportView.displayName = 'MoneyRequestReportView';
+
+export default MoneyRequestReportView;
diff --git a/src/components/Search/index.tsx b/src/components/Search/index.tsx
index ebeb5b8170cf..c5a10c43e76c 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 7a573412871b..05cc2f280ef9 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,
+ // This column is never displayed on Search
+ [CONST.REPORT.TRANSACTION_LIST.COLUMNS.COMMENTS]: () => false,
};
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,
},
];
@@ -104,51 +108,47 @@ 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: SortableColumnName) => {
+ 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 000000000000..ff6009bb1224
--- /dev/null
+++ b/src/components/SelectionList/SortableTableHeader.tsx
@@ -0,0 +1,67 @@
+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';
+import useThemeStyles from '@hooks/useThemeStyles';
+import CONST from '@src/CONST';
+import type {TranslationPaths} from '@src/languages/types';
+import SortableHeaderText from './SortableHeaderText';
+import type {SortableColumnName} from './types';
+
+type ColumnConfig = {
+ columnName: SortableColumnName;
+ translationKey: TranslationPaths | undefined;
+ isColumnSortable?: boolean;
+};
+
+type SearchTableHeaderProps = {
+ columns: ColumnConfig[];
+ sortBy?: SortableColumnName;
+ sortOrder?: SortOrder;
+ shouldShowSorting: boolean;
+ dateColumnSize: 'normal' | 'wide';
+ containerStyles?: StyleProp;
+ shouldShowColumn: (columnName: SortableColumnName) => boolean;
+ onSortPress: (column: SortableColumnName, order: SortOrder) => void;
+};
+
+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;
+ }
+
+ 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/SelectionList/types.ts b/src/components/SelectionList/types.ts
index c409c6a61d38..82226fe1bc51 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/components/TransactionItemRow/TransactionItemRowRBR.tsx b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx
new file mode 100644
index 000000000000..e9c1629e87d4
--- /dev/null
+++ b/src/components/TransactionItemRow/TransactionItemRowRBR.tsx
@@ -0,0 +1,48 @@
+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';
+import Text from '@components/Text';
+import useLocalize from '@hooks/useLocalize';
+import useTheme from '@hooks/useTheme';
+import useThemeStyles from '@hooks/useThemeStyles';
+import useTransactionViolations from '@hooks/useTransactionViolations';
+import ViolationsUtils from '@libs/Violations/ViolationsUtils';
+import variables from '@styles/variables';
+import type Transaction from '@src/types/onyx/Transaction';
+
+function TransactionItemRowRBR({transaction, containerStyles}: {transaction: Transaction; containerStyles?: ViewStyle[]}) {
+ const styles = useThemeStyles();
+ const transactionViolations = useTransactionViolations(transaction?.transactionID);
+ const {translate} = useLocalize();
+ const theme = useTheme();
+
+ const RBRmessages = transactionViolations
+ .map((violation, index) => {
+ 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 14428f934dde..ebce7c6eccfb 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,
@@ -29,6 +30,7 @@ function TransactionItemRow({
const StyleUtils = useStyleUtils();
const backgroundColor = isSelected ? styles.buttonDefaultBG : styles.highlightBG;
+ const hasCategoryOrTag = !!transactionItem.category || !!transactionItem.tag;
return (
@@ -71,7 +73,7 @@ function TransactionItemRow({
-
+
+
)}
) : (
{(hovered) => (
-
+
-
+
-
+
-
+
-
+
-
+
-
+
-
+
+
)}
diff --git a/src/hooks/useIsReportReadyToDisplay.ts b/src/hooks/useIsReportReadyToDisplay.ts
new file mode 100644
index 000000000000..15cdc5190bb3
--- /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/hooks/useLoadReportActions.ts b/src/hooks/useLoadReportActions.ts
new file mode 100644
index 000000000000..e4af06fd3332
--- /dev/null
+++ b/src/hooks/useLoadReportActions.ts
@@ -0,0 +1,129 @@
+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 UseLoadReportActionsArguments = {
+ /** The id of the current report */
+ reportID: string;
+
+ /** The id of the reportAction (if specific action was linked to */
+ reportActionID?: string;
+
+ /** 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 transaction, 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 reportActions.
+ * Used in the report displaying components
+ */
+function useLoadReportActions({reportID, reportActionID, reportActions, allReportActionIDs, transactionThreadReport, hasOlderActions, hasNewerActions}: UseLoadReportActionsArguments) {
+ 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 reportActions 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 reportActions 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 useLoadReportActions;
diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts
index 12517dedb5ea..d884b38b8fbd 100644
--- a/src/libs/ReportActionsUtils.ts
+++ b/src/libs/ReportActionsUtils.ts
@@ -542,12 +542,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) {
@@ -564,9 +577,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
@@ -614,6 +627,60 @@ function isConsecutiveActionMadeByPreviousActor(reportActions: ReportAction[] |
return currentAction.actorAccountID === previousAction.actorAccountID;
}
+// 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 (issue: https://github.com/Expensify/App/issues/58625)
+ 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));
@@ -2269,6 +2336,7 @@ export {
isClosedAction,
isConsecutiveActionMadeByPreviousActor,
isConsecutiveChronosAutomaticTimerAction,
+ hasNextActionMadeBySameActor,
isCreatedAction,
isCreatedTaskReportAction,
isCurrentActionUnread,
diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts
index e381fba1a0de..ade03988382f 100644
--- a/src/libs/SearchUIUtils.ts
+++ b/src/libs/SearchUIUtils.ts
@@ -7,7 +7,6 @@ import type {SearchColumnType, SearchStatus, SortOrder} from '@components/Search
import ChatListItem from '@components/SelectionList/ChatListItem';
import ReportListItem from '@components/SelectionList/Search/ReportListItem';
import TransactionListItem from '@components/SelectionList/Search/TransactionListItem';
-import {SearchColumns} from '@components/SelectionList/SearchTableHeader';
import type {ListItem, ReportActionListItemType, ReportListItemType, TransactionListItemType} from '@components/SelectionList/types';
import * as Expensicons from '@src/components/Icon/Expensicons';
import CONST from '@src/CONST';
@@ -794,7 +793,7 @@ function createBaseSavedSearchMenuItem(item: SaveSearchItem, key: string, index:
* Whether to show the empty state or not
*/
function shouldShowEmptyState(isDataLoaded: boolean, dataLength: number, type: SearchDataTypes) {
- return !isDataLoaded || dataLength === 0 || !Object.hasOwn(SearchColumns, type);
+ return !isDataLoaded || dataLength === 0 || !Object.values(CONST.SEARCH.DATA_TYPES).includes(type);
}
export {
diff --git a/src/pages/Search/SearchMoneyRequestReportPage.tsx b/src/pages/Search/SearchMoneyRequestReportPage.tsx
index a2af16a67a6e..57ff2a38dcff 100644
--- a/src/pages/Search/SearchMoneyRequestReportPage.tsx
+++ b/src/pages/Search/SearchMoneyRequestReportPage.tsx
@@ -1,76 +1,59 @@
-import React from 'react';
+import {PortalHost} from '@gorhom/portal';
+import React, {useEffect} from 'react';
import {View} from 'react-native';
-import type {OnyxEntry} from 'react-native-onyx';
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';
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 Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
import type {SearchFullscreenNavigatorParamList} from '@libs/Navigation/types';
import {buildSearchQueryJSON} from '@libs/SearchQueryUtils';
+import {openReport} from '@userActions/Report';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
-import type * as OnyxTypes from '@src/types/onyx';
import SearchTypeMenu from './SearchTypeMenu';
-type SearchPageProps = PlatformStackScreenProps;
-
// 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 [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 {isEditingDisabled, isCurrentReportLoadedFromOnyx} = useIsReportReadyToDisplay(report, reportID);
+
+ useEffect(() => {
+ openReport(reportID);
+ }, [reportID]);
+
if (shouldUseNarrowLayout) {
return (
-
);
@@ -106,10 +91,19 @@ function SearchMoneyRequestReportPage({route}: SearchPageProps) {
-
+
+
+
+
+
+
+
+
);
diff --git a/src/pages/home/ReportScreen.tsx b/src/pages/home/ReportScreen.tsx
index f29a4d85fc56..1e859776d93f 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';
@@ -148,7 +149,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,
@@ -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 ? (
void;
-
/** Should enable auto scroll to top threshold */
shouldEnableAutoScrollToTopThreshold?: boolean;
};
@@ -154,7 +151,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],
- );
const retryLoadNewerChatsError = useCallback(() => {
loadNewerChats(true);
@@ -768,7 +758,6 @@ function ReportActionsList({
ListFooterComponent={listFooterComponent}
keyboardShouldPersistTaps="handled"
onLayout={onLayoutInner}
- onContentSizeChange={onContentSizeChangeInner}
onScroll={trackVerticalScrolling}
onScrollToIndexFailed={onScrollToIndexFailed}
extraData={extraData}
diff --git a/src/pages/home/report/ReportActionsListItemRenderer.tsx b/src/pages/home/report/ReportActionsListItemRenderer.tsx
index 1a0d8db13661..eaceaf1df35e 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/pages/home/report/ReportActionsView.tsx b/src/pages/home/report/ReportActionsView.tsx
index 70f953f49d91..f5b6f04b5a2d 100755
--- a/src/pages/home/report/ReportActionsView.tsx
+++ b/src/pages/home/report/ReportActionsView.tsx
@@ -5,10 +5,11 @@ import type {OnyxEntry} from 'react-native-onyx';
import {useOnyx} from 'react-native-onyx';
import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';
import useCopySelectionHelper from '@hooks/useCopySelectionHelper';
+import useLoadReportActions from '@hooks/useLoadReportActions';
import useNetwork from '@hooks/useNetwork';
import usePrevious from '@hooks/usePrevious';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
-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';
@@ -84,12 +85,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);
@@ -200,21 +198,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
@@ -222,78 +211,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} = useLoadReportActions({
+ reportID,
+ reportActionID,
+ reportActions,
+ allReportActionIDs,
+ transactionThreadReport,
+ hasOlderActions,
+ hasNewerActions,
+ });
/**
* Runs when the FlatList finishes laying out
@@ -364,7 +294,6 @@ function ReportActionsView({
loadOlderChats={loadOlderChats}
loadNewerChats={loadNewerChats}
listID={listID}
- onContentSizeChange={onContentSizeChange}
shouldEnableAutoScrollToTopThreshold={shouldEnableAutoScroll}
/>
diff --git a/src/styles/utils/index.ts b/src/styles/utils/index.ts
index b6ee3085d981..a7f7b537e359 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:
diff --git a/tests/perf-test/ReportActionsList.perf-test.tsx b/tests/perf-test/ReportActionsList.perf-test.tsx
index dff140a3e63a..2dd7ed401fc2 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}