Skip to content
Open
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
9 changes: 8 additions & 1 deletion src/hooks/useConciergeSidePanelReportActions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import DateUtils from '@libs/DateUtils';
import {isCreatedAction, isCurrentUserPendingAddAction} from '@libs/ReportActionsUtils';
import {buildConciergeGreetingReportAction} from '@libs/ReportUtils';

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

import type {OnyxEntry} from 'react-native-onyx';
Expand Down Expand Up @@ -144,7 +145,13 @@ function useConciergeSidePanelReportActions({
return false;
}
if (isConciergeMainDM) {
return isCreatedAction(action) || isCurrentUserPendingAddAction(action, currentUserAccountID) || action.created >= sessionStartTime;
// A still-OPEN child task (e.g. an unfinished onboarding task) stays pinned even though it predates
// the session, so collapsing read history behind "Show history" never buries a task the user still
// has to act on. This replaces the blanket `hasOutstandingChildTask` bypass that used to force the
// entire history open, which is what suppressed the "Show history" button altogether.
const isOpenChildTask =
action.childType === CONST.REPORT.TYPE.TASK && action.childStateNum === CONST.REPORT.STATE_NUM.OPEN && action.childStatusNum === CONST.REPORT.STATUS_NUM.OPEN;
return isCreatedAction(action) || isCurrentUserPendingAddAction(action, currentUserAccountID) || isOpenChildTask || action.created >= sessionStartTime;
Comment on lines +152 to +154

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve pending tasks while the welcome view is active

When a user opens the Concierge DM in a fresh session without sending a new message, showConciergeSidePanelWelcome is true and filterActions() returns only the greeting and CREATED action before this new predicate is evaluated. Consequently, even a loaded open task that was previously visible because hasOutstandingChildTask enabled full history is now hidden until the user expands history; the added test avoids this path by inserting an in-session user message. The welcome branch must also retain open child tasks.

Useful? React with 👍 / 👎.

}
if (!firstUserMessageCreated) {
return false;
Expand Down
2 changes: 1 addition & 1 deletion src/hooks/useReportActionsListModel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ function useReportActionsListModel(reportID: string, isReportLoadPending: boolea
hasOlderActions,
loadOlderChats,
mainDMSessionStartTime: sessionStartTime,
conciergeShowFullHistory: conciergeShowFullHistory || !!reportActionIDFromRoute || !!report?.hasOutstandingChildTask,
conciergeShowFullHistory: conciergeShowFullHistory || !!reportActionIDFromRoute,

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Load the page containing an outstanding task before filtering

For Concierge reports whose open task action is older than the current pagination chain, removing report.hasOutstandingChildTask here leaves hidden-history mode enabled while the new task predicate can inspect only the paginated reportActions. In that mode ReportActionsList.loadOlderChatsOnEndReached() deliberately refuses to load older pages, so the outstanding task cannot be pinned and remains unavailable until the user manually selects Show history. Preserve a targeted way to load or obtain the outstanding task before collapsing the remaining history.

Useful? React with 👍 / 👎.

setConciergeShowFullHistory,
conciergeHadMessagesAtSessionStart,
setConciergeHadMessagesAtSessionStart,
Expand Down
86 changes: 84 additions & 2 deletions tests/ui/ReportActionsListTest.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -780,7 +780,7 @@ describe('ReportActionsList (body)', () => {
},
];

const setupMainDMConciergeMocks = (sessionStartTime: string | null = SESSION_START, showFullHistory = false, hasOnceLoadedReportActions = true) => {
const setupMainDMConciergeMocks = (sessionStartTime: string | null = SESSION_START, showFullHistory = false, hasOnceLoadedReportActions = true, hasOutstandingChildTask = false) => {
jest.spyOn(ReportActionsUtils, 'shouldReportActionBeVisible').mockReturnValue(true);
mockUseNetwork.mockReturnValue({isOffline: false});
mockUseIsInSidePanel.mockReturnValue(false);
Expand All @@ -805,7 +805,7 @@ describe('ReportActionsList (body)', () => {
return [[], {status: 'loaded'}];
}
if (key === `${ONYXKEYS.COLLECTION.REPORT}${CONCIERGE_REPORT_ID}`) {
return [{...mockReport, reportID: CONCIERGE_REPORT_ID}, {status: 'loaded'}];
return [{...mockReport, reportID: CONCIERGE_REPORT_ID, hasOutstandingChildTask}, {status: 'loaded'}];
}
if (key.includes('report')) {
return [undefined, {status: 'loaded'}];
Expand All @@ -832,6 +832,88 @@ describe('ReportActionsList (body)', () => {
expect(passedActions?.some((a) => a.reportActionID === 'old-concierge-msg')).toBe(false);
});

it('should keep read history hidden when the Concierge DM still has an outstanding child task', () => {
// Regression guard: an incomplete onboarding task used to force `showFullHistory` on permanently,
// which both un-hid the history and suppressed the "Show history" button.
setupMainDMConciergeMocks(SESSION_START, false, true, true);

mockUsePaginatedReportActions.mockReturnValue({
...defaultPaginatedReportActionsResult,
reportActions: oldReportActions,
hasOlderActions: false,
});

renderReportActionsList({reportID: CONCIERGE_REPORT_ID});

expect(mockInvertedFlashList).toHaveBeenCalled();
const passedActions = getCapturedVisibleActions();
expect(passedActions?.some((a) => a.reportActionID === CONST.CONCIERGE_GREETING_ACTION_ID)).toBe(true);
expect(passedActions?.some((a) => a.reportActionID === 'old-user-msg')).toBe(false);
expect(passedActions?.some((a) => a.reportActionID === 'old-concierge-msg')).toBe(false);
});

it('should keep a still-open child task visible while the rest of the read history stays hidden', () => {
setupMainDMConciergeMocks(SESSION_START, false, true, true);

// The session filter keys off the child* fields the backend stamps on a task's parent action.
const buildTaskAction = (
reportActionID: string,
created: string,
stateNum: OnyxTypes.ReportAction['childStateNum'],
statusNum: OnyxTypes.ReportAction['childStatusNum'],
): OnyxTypes.ReportAction => ({
reportActionID,
actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
created,
actorAccountID: 456,
message: [{type: 'COMMENT', html: 'Take a test drive', text: 'Take a test drive'}],
originalMessage: {},
childType: CONST.REPORT.TYPE.TASK,
childReportID: `task-${reportActionID}`,
childStateNum: stateNum,
childStatusNum: statusNum,
shouldShow: true,
person: [{type: 'TEXT', style: 'strong', text: 'Concierge'}],
pendingAction: null,
errors: {},
});

// An in-session message keeps the list out of welcome mode, so the session filter actually runs.
const newUserMessage: OnyxTypes.ReportAction = {
reportActionID: 'new-user-msg',
actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
created: '2024-06-01 12:05:00.000',
actorAccountID: CURRENT_USER_ACCOUNT_ID,
message: [{type: 'COMMENT', html: 'Hello', text: 'Hello'}],
originalMessage: {},
shouldShow: true,
person: [{type: 'TEXT', style: 'strong', text: 'Test User'}],
pendingAction: null,
errors: {},
};

mockUsePaginatedReportActions.mockReturnValue({
...defaultPaginatedReportActionsResult,
reportActions: [
...oldReportActions,
buildTaskAction('open-task', '2023-06-15 10:02:00.000', CONST.REPORT.STATE_NUM.OPEN, CONST.REPORT.STATUS_NUM.OPEN),
buildTaskAction('completed-task', '2023-06-15 10:03:00.000', CONST.REPORT.STATE_NUM.APPROVED, CONST.REPORT.STATUS_NUM.APPROVED),
newUserMessage,
],
hasOlderActions: false,
});

renderReportActionsList({reportID: CONCIERGE_REPORT_ID});

expect(mockInvertedFlashList).toHaveBeenCalled();
const passedActions = getCapturedVisibleActions();
expect(passedActions?.some((a) => a.reportActionID === 'new-user-msg')).toBe(true);
expect(passedActions?.some((a) => a.reportActionID === 'open-task')).toBe(true);
expect(passedActions?.some((a) => a.reportActionID === 'completed-task')).toBe(false);
expect(passedActions?.some((a) => a.reportActionID === 'old-user-msg')).toBe(false);
expect(passedActions?.some((a) => a.reportActionID === 'old-concierge-msg')).toBe(false);
});

it('should show all actions when showFullHistory is true', () => {
setupMainDMConciergeMocks(SESSION_START, true);

Expand Down
75 changes: 75 additions & 0 deletions tests/unit/hooks/useConciergeSidePanelReportActions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -129,3 +129,78 @@ describe('useConciergeSidePanelReportActions (clock-skew reply visibility)', ()
expect(visibleIDs).not.toContain('21');
});
});

describe('useConciergeSidePanelReportActions (main DM open-task pinning)', () => {
const SESSION_START = toDBTime(CLIENT_OPEN_MS);

/** Builds the parent action of a task, carrying the child* fields the backend stamps on it. */
function buildTaskAction(reportActionID: string, stateNum: ReportAction['childStateNum'], statusNum: ReportAction['childStatusNum']): ReportAction {
return buildAction(reportActionID, {
actorAccountID: CONCIERGE_ACCOUNT_ID,
created: toDBTime(CLIENT_OPEN_MS - 3_400_000),
childType: CONST.REPORT.TYPE.TASK,
childReportID: `task-${reportActionID}`,
childStateNum: stateNum,
childStatusNum: statusNum,
});
}

function renderMainDM(taskAction: ReportAction) {
const createdAction = buildAction('10', {actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, created: toDBTime(CLIENT_OPEN_MS - 7_200_000)});
const preSessionUser = buildAction('11', {created: toDBTime(CLIENT_OPEN_MS - 3_600_000)});
const preSessionConcierge = buildAction('12', {actorAccountID: CONCIERGE_ACCOUNT_ID, created: toDBTime(CLIENT_OPEN_MS - 3_500_000)});
const inSessionUser = buildAction('20', {created: toDBTime(CLIENT_OPEN_MS + 1000)});
const reportActions = [createdAction, preSessionUser, preSessionConcierge, taskAction, inSessionUser];

// `hasOutstandingChildTask` no longer reaches this hook as `showFullHistory` — the pin below is what keeps
// the task reachable, so the rest of the read history can stay collapsed behind "Show history".
const report: Report = {...createRandomReport(Number(REPORT_ID)), reportID: REPORT_ID, lastReadTime: SESSION_START, hasOutstandingChildTask: true};

return renderHook(() =>
useConciergeSidePanelReportActions({
report,
reportActions,
visibleReportActions: reportActions,
isConciergeHiddenHistory: true,
hasUserSentMessage: true,
hasOlderActions: false,
sessionStartTime: SESSION_START,
currentUserAccountID: CURRENT_USER_ACCOUNT_ID,
greetingText: 'Hi there, how can I help?',
loadOlderChats: jest.fn(),
isConciergeMainDM: true,
showFullHistory: false,
hadMessagesAtSessionStart: true,
}),
);
}

it('keeps a still-open child task visible while the rest of the read history stays hidden', () => {
// Given a pre-session task that the user has not completed yet
const {result} = renderMainDM(buildTaskAction('30', CONST.REPORT.STATE_NUM.OPEN, CONST.REPORT.STATUS_NUM.OPEN));

// When the main DM filters the session's actions
const visibleIDs = result.current.filteredReportActions.map((action) => action.reportActionID);

// Then the task stays pinned, the read history stays hidden, and "Show history" is still offered.
expect(visibleIDs).toContain('30');
expect(visibleIDs).toContain('20');
expect(visibleIDs).not.toContain('11');
expect(visibleIDs).not.toContain('12');
expect(result.current.showFullHistory).toBe(false);
expect(result.current.hasPreviousMessages).toBe(true);
});

it('hides a completed child task along with the rest of the read history', () => {
// Given a pre-session task that has already been completed
const {result} = renderMainDM(buildTaskAction('30', CONST.REPORT.STATE_NUM.APPROVED, CONST.REPORT.STATUS_NUM.APPROVED));

// When the main DM filters the session's actions
const visibleIDs = result.current.filteredReportActions.map((action) => action.reportActionID);

// Then it is treated as ordinary read history and collapses behind "Show history".
expect(visibleIDs).not.toContain('30');
expect(visibleIDs).not.toContain('11');
expect(visibleIDs).toContain('20');
});
});
Loading