Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
729 changes: 0 additions & 729 deletions src/pages/inbox/report/ReportActionsList.tsx

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,273 @@
import {renderScrollComponent as renderActionSheetAwareScrollView} from '@components/ActionSheetAwareScrollView';
import InvertedFlashList from '@components/FlashList/InvertedFlashList';
import MerchantRuleSuggestionBanner from '@components/MerchantRuleSuggestionBanner';
import {ReportActionsAnimatedSkeletonCover} from '@components/ReportActionsSkeletonCover';
import ReportActionsSkeletonView from '@components/ReportActionsSkeletonView';

import useEnvironment from '@hooks/useEnvironment';
import useLinkedMessageOfflineLoading from '@hooks/useLinkedMessageOfflineLoading';
import useLocalize from '@hooks/useLocalize';
import useMarkAsRead from '@hooks/useMarkAsRead';
import useNetwork from '@hooks/useNetwork';
import useOnyx from '@hooks/useOnyx';
import useRetireMerchantRuleSuggestionOnLeave from '@hooks/useRetireMerchantRuleSuggestionOnLeave';
import useSidePanelState from '@hooks/useSidePanelState';
import useThemeStyles from '@hooks/useThemeStyles';
import useUnreadMarker from '@hooks/useUnreadMarker';

import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackRouteProp} from '@libs/Navigation/PlatformStackNavigation/types';
import {shouldHidePayAction} from '@libs/ReportActionsUtils';
import {isArchivedReport, shouldShowMarkAsDone} from '@libs/ReportUtils';

import type {ReportsSplitNavigatorParamList} from '@navigation/types';

import {useActionListContext} from '@pages/inbox/ActionListContext';
import {useConciergeChatHistoryActions} from '@pages/inbox/ConciergeChatHistoryContext';
import AskConciergeWelcome from '@pages/inbox/report/AskConciergeWelcome';
import ConciergeChatHistoryToggle from '@pages/inbox/report/ConciergeChatHistoryToggle';
import FloatingMessageCounter from '@pages/inbox/report/FloatingMessageCounter';
import {useReportActionsListActions, useReportActionsListState} from '@pages/inbox/report/ReportActionsListContext';
import ReportActionsListHeader from '@pages/inbox/report/ReportActionsListHeader';
import ReportActionsListPaddingView from '@pages/inbox/report/ReportActionsListPaddingView';

import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type SCREENS from '@src/SCREENS';
import type * as OnyxTypes from '@src/types/onyx';

import type {OnyxEntry} from 'react-native-onyx';

import {useRoute} from '@react-navigation/native';
import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import React, {useState} from 'react';

import type {ReportActionsListProps} from './types';

import useReportActionsListData from './useReportActionsListData';
import useReportActionsListRenderItem from './useReportActionsListRenderItem';
import useReportActionsListScroll from './useReportActionsListScroll';
import useReportActionsListTelemetry from './useReportActionsListTelemetry';

/**
* Renders the report-actions list. Reads its data from `ReportActionsListStateContext` / `ReportActionsListActionsContext` and holds the
* UI-close hooks (`useUnreadMarker` / `useMarkAsRead` / `useReportActionsScroll`). `ReportActionsSkeletonGuard`
* mounts it only once content is ready, so those hooks never run while a skeleton shows.
*/
function ReportActionsListContent({reportID, conciergeChat, onLayout}: ReportActionsListProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const {isProduction} = useEnvironment();

const {
report,
hasOnceLoadedReportActions,
hasNewerActions,
oldestUnreadReportAction,
transactionThreadReport,
sortedReportActions: unfilteredReportActions,
sortedVisibleReportActions: unfilteredVisibleReportActions,
isConciergeHiddenHistory,
shouldShowConciergeWelcome,
isConciergeHistoryExpanded,
} = useReportActionsListState();

// Remove rows hidden by the renderer before calculating summary counts, headers, and scroll positions.
const sortedReportActions = report?.isWaitingOnBankAccount ? unfilteredReportActions.filter((action) => !shouldHidePayAction(action, true)) : unfilteredReportActions;
const sortedVisibleReportActions = report?.isWaitingOnBankAccount
? unfilteredVisibleReportActions.filter((action) => !shouldHidePayAction(action, true))
: unfilteredVisibleReportActions;

const {handleShowPreviousMessages: onShowPreviousMessages} = useReportActionsListActions();

const {isOffline} = useNetwork();
const route = useRoute<PlatformStackRouteProp<ReportsSplitNavigatorParamList, typeof SCREENS.REPORT>>();
const linkedReportActionID = route?.params?.reportActionID;
const {sessionStartTime} = useSidePanelState();
const {collapseHistory: collapseConciergeHistory} = useConciergeChatHistoryActions();

const recordTimeToMeasureItemLayout = useReportActionsListTelemetry({reportID, onLayout});

useLinkedMessageOfflineLoading({reportID: report?.reportID ?? reportID, reportActionIDFromRoute: linkedReportActionID});

// Owned here rather than by the callout, which unmounts as the layout and composer change size.
useRetireMerchantRuleSuggestionOnLeave(reportID);

const [reportNameValuePairs] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`);
const isReportArchived = !!isArchivedReport(reportNameValuePairs);
const [isTrackIntentUser] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {selector: isTrackIntentUserSelector});
const [policy] = useOnyx(`${ONYXKEYS.COLLECTION.POLICY}${getNonEmptyStringOnyxID(report?.policyID)}`);
const [rules] = useOnyx(ONYXKEYS.COLLECTION.RULE);

const reportAttributesSelector = (value: OnyxEntry<OnyxTypes.ReportAttributesDerivedValue>) => {
const attrs = value?.reports?.[reportID];
if (!attrs) {
return undefined;
}
return {
actionBadge: attrs.actionBadge,
actionTargetReportActionID: attrs.actionTargetReportActionID,
brickRoadStatus: attrs.brickRoadStatus,
};
};
const [reportAttributes] = useOnyx(ONYXKEYS.DERIVED.REPORT_ATTRIBUTES, {
selector: reportAttributesSelector,
});

const {getScrollOffset} = useActionListContext();
const [hasScrolledOverThreshold, setHasScrolledOverThreshold] = useState(() => getScrollOffset() >= CONST.REPORT.ACTIONS.ACTION_VISIBLE_THRESHOLD);

const {unreadMarkerReportActionID} = useUnreadMarker({
reportID,
sortedVisibleReportActions,
sortedReportActions,
oldestUnreadReportActionID: oldestUnreadReportAction?.reportActionID,
isScrolledOverThreshold: hasScrolledOverThreshold,
hasOnceLoadedReportActions: !!hasOnceLoadedReportActions,
newMessageBoundaryTime: isConciergeHiddenHistory ? sessionStartTime : undefined,
});

const {markNewestActionAsRead, completeSkippedMarkAsRead} = useMarkAsRead({
reportID,
report,
transactionThreadReport,
sortedVisibleReportActions,
isScrolledToEnd: !hasScrolledOverThreshold,
hasNewerActions,
});

const reportActionsListData = useReportActionsListData({
sortedVisibleReportActions,
reportNameValuePairs,
sessionStartTime,
linkedReportActionID,
unreadMarkerReportActionID,
});
const {listData, reportActionIDToDisplayIndex, unreadMarkerListIndex, focusRequestedReportActionID, draftAutoScrollKey, isDraftPendingCompletion} = reportActionsListData;

const {listID, listProps, shouldBeAlignedToTop, isFloatingMessageCounterVisible, isActionBadgeAboveViewport, scrollToBottomAndMarkReportAsRead, scrollToActionBadgeTarget} =
useReportActionsListScroll({
reportID,
conciergeChat,
sortedVisibleReportActions,
renderedVisibleReportActions: listData,
reportActionIDToDisplayIndex,
hasScrolledOverThreshold,
markNewestActionAsRead,
completeSkippedMarkAsRead,
unreadMarkerReportActionID,
unreadMarkerReportActionIndex: unreadMarkerListIndex,
draftAutoScrollKey,
focusRequestedReportActionID,
actionBadgeTargetID: reportAttributes?.actionTargetReportActionID,
linkedReportActionID,
onScrolledOverThresholdChange: setHasScrolledOverThreshold,
onLayout: recordTimeToMeasureItemLayout,
});

const {renderItem, extraData} = useReportActionsListRenderItem({
...reportActionsListData,
reportID,
reportNameValuePairs,
isReportArchived,
isOffline,
sortedReportActions,
unreadMarkerReportActionID,
linkedReportActionID,
onShowPreviousMessages,
});

const listHeaderComponent = (
<>
{isConciergeHistoryExpanded && (
<ConciergeChatHistoryToggle
isHistoryShown
onPress={() => {
collapseConciergeHistory();
if (linkedReportActionID) {
Navigation.setParams({reportActionID: ''}, route.key);
}
}}
/>
)}
<ReportActionsListHeader
reportID={reportID}
isDraftPendingCompletion={isDraftPendingCompletion}
/>
</>
);

const shouldShowOfflineSkeleton = isOffline && !sortedVisibleReportActions.some((action) => action.actionName === CONST.REPORT.ACTIONS.TYPE.CREATED);

const listFooterComponent = shouldShowOfflineSkeleton ? <ReportActionsSkeletonView shouldAnimate={false} /> : undefined;

const shouldShowMarkAsDoneCopy = shouldShowMarkAsDone({
policy,
report,
isTrackIntentUser,
rules,
});

// The guard only mounts this content when the report is loaded, so this is effectively unreachable.
// It narrows `report` to non-undefined for the render below and stays a safe fallback if the report
// is cleared mid-session while the latch keeps the content mounted.
if (!report) {
return <ReportActionsAnimatedSkeletonCover />;
}

if (shouldShowConciergeWelcome) {
return (
<AskConciergeWelcome
reportID={reportID}
onShowPreviousMessages={onShowPreviousMessages}
/>
);
}

return (
<>
{/* Pinned over the top of the list rather than laid out inside it, so scrolling the expense detail view
does not carry it out of sight. Renders nothing on the layouts the composer mount serves. */}
<MerchantRuleSuggestionBanner
reportID={reportID}
policyID={report?.policyID}
containerStyles={[styles.mh4, styles.mt2]}
overlayStyles={styles.merchantRuleCalloutOverlay}
/>
<FloatingMessageCounter
hasNewMessages={!!unreadMarkerReportActionID}
isActive={isFloatingMessageCounterVisible}
onClick={scrollToBottomAndMarkReportAsRead}
actionBadge={!isProduction && isActionBadgeAboveViewport ? reportAttributes?.actionBadge : undefined}
actionBadgeBrickRoadStatus={!isProduction && isActionBadgeAboveViewport ? reportAttributes?.brickRoadStatus : undefined}
onActionBadgePress={scrollToActionBadgeTarget}
shouldShowMarkAsDoneCopy={shouldShowMarkAsDoneCopy}
/>
<ReportActionsListPaddingView
report={report}
isReportArchived={isReportArchived}
>
<InvertedFlashList
{...listProps}
accessibilityLabel={translate('sidebarScreen.listOfChatMessages')}
testID="report-actions-list"
style={styles.overscrollBehaviorContain}
data={listData}
renderItem={renderItem}
drawDistance={1500}
renderScrollComponent={renderActionSheetAwareScrollView}
contentContainerStyle={styles.chatContentScrollView}
ListHeaderComponent={listHeaderComponent}
ListHeaderComponentStyle={shouldBeAlignedToTop ? styles.flex1 : undefined}
ListFooterComponent={listFooterComponent}
keyboardShouldPersistTaps="handled"
extraData={extraData}
key={listID}
/>
</ReportActionsListPaddingView>
</>
);
}

export default ReportActionsListContent;
26 changes: 26 additions & 0 deletions src/pages/inbox/report/ReportActionsList/index.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import ReportActionsSkeletonGuard from '@pages/inbox/report/ReportActionsSkeletonGuard';

import React from 'react';

import type {ReportActionsListProps} from './types';

import ReportActionsListContent from './ReportActionsListContent';

/**
* Public report-actions list. Thin composition that wraps the content in `ReportActionsSkeletonGuard`,
* which owns the data pipeline + skeleton decision and only mounts the content once it is ready.
*/
function ReportActionsList({reportID, conciergeChat, onLayout}: ReportActionsListProps) {
return (
<ReportActionsSkeletonGuard reportID={reportID}>
<ReportActionsListContent
key={reportID}
reportID={reportID}
conciergeChat={conciergeChat}
onLayout={onLayout}
/>
</ReportActionsSkeletonGuard>
);
}

export default ReportActionsList;
72 changes: 72 additions & 0 deletions src/pages/inbox/report/ReportActionsList/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
import type useReportActionsScroll from '@hooks/useReportActionsScroll';

import type useReportActionsPresentation from '@pages/inbox/report/useReportActionsPresentation';

import type * as OnyxTypes from '@src/types/onyx';

import type {LayoutChangeEvent} from 'react-native';
import type {OnyxEntry} from 'react-native-onyx';

import type useReportActionsListData from './useReportActionsListData';

type ReportActionsListProps = {
/** The ID of the report to display actions for */
reportID: string;

conciergeChat: OnyxEntry<OnyxTypes.Report>;
onLayout?: (event: LayoutChangeEvent) => void;
};

type UseReportActionsListDataParams = {
sortedVisibleReportActions: OnyxTypes.ReportAction[];
reportNameValuePairs: OnyxEntry<OnyxTypes.ReportNameValuePairs>;
sessionStartTime: string | null;
linkedReportActionID: string | undefined;
unreadMarkerReportActionID: string | null;
};

type UseReportActionsListScrollParams = Pick<
Parameters<typeof useReportActionsScroll>[0],
| 'reportID'
| 'conciergeChat'
| 'sortedVisibleReportActions'
| 'renderedVisibleReportActions'
| 'hasScrolledOverThreshold'
| 'markNewestActionAsRead'
| 'completeSkippedMarkAsRead'
| 'unreadMarkerReportActionID'
| 'unreadMarkerReportActionIndex'
| 'draftAutoScrollKey'
> &
Pick<ReturnType<typeof useReportActionsPresentation>, 'reportActionIDToDisplayIndex' | 'focusRequestedReportActionID'> &
Pick<UseReportActionsListDataParams, 'linkedReportActionID'> &
Pick<ReportActionsListProps, 'onLayout'> & {
actionBadgeTargetID: string | undefined;
onScrolledOverThresholdChange: (isOverThreshold: boolean) => void;
};

type UseReportActionsListRenderItemParams = Pick<
ReturnType<typeof useReportActionsListData>,
| 'renderedVisibleReportActions'
| 'listData'
| 'canonicalIndexByReportActionID'
| 'runsByAnchorReportActionID'
| 'expandedSystemMessageReportActionIDs'
| 'expandSystemMessageRun'
| 'focusRequestedReportActionID'
| 'onExpandedSystemMessageReady'
| 'draftMessageHTML'
| 'draftReportActionID'
| 'isDraftPendingCompletion'
> &
Pick<UseReportActionsListDataParams, 'reportNameValuePairs' | 'linkedReportActionID' | 'unreadMarkerReportActionID'> &
Pick<ReportActionsListProps, 'reportID'> & {
isReportArchived: boolean;
isOffline: boolean;
sortedReportActions: OnyxTypes.ReportAction[];
onShowPreviousMessages: () => void;
};

type UseReportActionsListTelemetryParams = Pick<ReportActionsListProps, 'reportID' | 'onLayout'>;

export type {ReportActionsListProps, UseReportActionsListDataParams, UseReportActionsListScrollParams, UseReportActionsListRenderItemParams, UseReportActionsListTelemetryParams};
Loading
Loading