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
1 change: 1 addition & 0 deletions src/pages/inbox/report/FloatingMessageCounter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -185,3 +185,4 @@ function FloatingMessageCounter({
FloatingMessageCounter.displayName = 'FloatingMessageCounter';

export default FloatingMessageCounter;
export type {FloatingMessageCounterProps};
Original file line number Diff line number Diff line change
Expand Up @@ -15,19 +15,17 @@ 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 {isArchivedReport} 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';
Expand All @@ -40,12 +38,12 @@ 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 filterHiddenSupportTicketSurveys from './filterHiddenSupportTicketSurveys';
import ReportActionsListFloatingMessageCounter from './ReportActionsListFloatingMessageCounter';
import useReportActionsListData from './useReportActionsListData';
import useReportActionsListRenderItem from './useReportActionsListRenderItem';
import useReportActionsListScroll from './useReportActionsListScroll';
Expand Down Expand Up @@ -111,9 +109,6 @@ function ReportActionsListContent({reportID, onLayout}: ReportActionsListProps)
reportNameValuePairs,
});
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];
Expand Down Expand Up @@ -216,13 +211,6 @@ function ReportActionsListContent({reportID, onLayout}: ReportActionsListProps)

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.
Expand All @@ -249,14 +237,14 @@ function ReportActionsListContent({reportID, onLayout}: ReportActionsListProps)
containerStyles={[styles.mh4, styles.mt2]}
overlayStyles={styles.merchantRuleCalloutOverlay}
/>
<FloatingMessageCounter
<ReportActionsListFloatingMessageCounter
report={report}
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}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/** Keep counter copy subscriptions separate from the report-actions list's rendering work. */
import useOnyx from '@hooks/useOnyx';

import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
import {shouldShowMarkAsDone} from '@libs/ReportUtils';

import FloatingMessageCounter from '@pages/inbox/report/FloatingMessageCounter';
import type {FloatingMessageCounterProps} from '@pages/inbox/report/FloatingMessageCounter';

import ONYXKEYS from '@src/ONYXKEYS';
import type {Report} from '@src/types/onyx';

import {isTrackIntentUserSelector} from '@selectors/Onboarding';
import React from 'react';

type ReportActionsListFloatingMessageCounterProps = Omit<FloatingMessageCounterProps, 'shouldShowMarkAsDoneCopy'> & {
report: Report;
};

function ReportActionsListFloatingMessageCounter({report, ...counterProps}: ReportActionsListFloatingMessageCounterProps) {
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 shouldShowMarkAsDoneCopy = shouldShowMarkAsDone({policy, report, isTrackIntentUser, rules});

return (
<FloatingMessageCounter
{...counterProps}
shouldShowMarkAsDoneCopy={shouldShowMarkAsDoneCopy}
/>
);
}

export default ReportActionsListFloatingMessageCounter;
31 changes: 30 additions & 1 deletion tests/ui/ReportActionsListTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import {useConciergeChatHistoryActions, useConciergeChatHistoryState} from '@pag
import {useConciergeDraft, useConciergeDraftActions} from '@pages/inbox/ConciergeDraftContext';
import CollapsedSystemMessages from '@pages/inbox/report/CollapsedSystemMessages';
import ReportActionsList from '@pages/inbox/report/ReportActionsList';
import ReportActionsListFloatingMessageCounter from '@pages/inbox/report/ReportActionsList/ReportActionsListFloatingMessageCounter';
import ReportActionsListItemRenderer from '@pages/inbox/report/ReportActionsListItemRenderer';
import {getUnreadMarkerReportAction} from '@pages/inbox/report/shouldDisplayNewMarkerOnReportAction';
import * as ReportActionsNewActionLiveTail from '@pages/inbox/report/useReportActionsNewActionLiveTail';
Expand Down Expand Up @@ -178,7 +179,7 @@ jest.mock('@hooks/useReportActionsScroll', () =>
onLoad: jest.fn(),
})),
);
jest.mock('@pages/inbox/report/FloatingMessageCounter', () => jest.fn(() => null));
jest.mock('@pages/inbox/report/ReportActionsList/ReportActionsListFloatingMessageCounter', () => jest.fn(() => null));
jest.mock('@pages/inbox/report/ReportActionsListPaddingView', () => {
const reactModule = jest.requireActual<typeof React>('react');
return jest.fn(({children}: {children: React.ReactNode}) => reactModule.createElement(reactModule.Fragment, null, children));
Expand Down Expand Up @@ -377,6 +378,34 @@ describe('ReportActionsList (body)', () => {
await Onyx.clear();
});

it('leaves counter copy subscriptions in the counter wrapper while forwarding report and navigation props', () => {
// Given loaded list content with its counter mocked, copy-only reads must not remain in the parent or model hooks.
const reportWithPolicy = {...mockReport, policyID: 'counter-policy'};
mockUseNetwork.mockReturnValue({isOffline: false});
mockUseOnyx.mockImplementation((key, options) =>
key === `${ONYXKEYS.COLLECTION.REPORT}${mockReport.reportID}` ? [reportWithPolicy, {status: 'loaded'}] : getMockOnyxValue(key, options),
);
mockUseUnreadMarker.mockReturnValue({unreadMarkerReportActionID: mockReportActions[1].reportActionID, unreadMarkerReportActionIndex: 1});

// When the public list mounts its loaded content, the wrapper should receive the same report and scroll handlers.
renderReportActionsList();

// Then the list is mounted, but none of the counter's copy subscriptions have leaked above the mocked wrapper.
expect(mockInvertedFlashList).toHaveBeenCalled();
const subscribedKeys = mockUseOnyx.mock.calls.map(([key]) => key);
expect(subscribedKeys).not.toContain(`${ONYXKEYS.COLLECTION.POLICY}${reportWithPolicy.policyID}`);
expect(subscribedKeys).not.toContain(ONYXKEYS.COLLECTION.RULE);
expect(subscribedKeys).not.toContain(ONYXKEYS.NVP_INTRO_SELECTED);
const counterProps = jest.mocked(ReportActionsListFloatingMessageCounter).mock.lastCall?.[0];
const scrollResult = mockUseReportActionsScroll.mock.results.at(-1)?.value as ReturnType<typeof useReportActionsScroll> | undefined;
expect(counterProps?.report).toBe(reportWithPolicy);
expect(counterProps?.hasNewMessages).toBe(true);
expect(counterProps?.isActive).toBe(false);
expect(counterProps?.onClick).toBe(scrollResult?.scrollToBottomAndMarkReportAsRead);
expect(counterProps?.onActionBadgePress).toBe(scrollResult?.scrollToActionBadgeTarget);
expect(counterProps).not.toHaveProperty('shouldShowMarkAsDoneCopy');
});

describe('Support survey unread markers', () => {
it.each([
{hasUnreadMarker: true, hasNewestPage: true},
Expand Down
Loading
Loading