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
21 changes: 10 additions & 11 deletions src/libs/ReportUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -884,7 +884,7 @@
const parsedReportActionMessageCache: Record<string, string> = {};

let conciergeReportID: OnyxEntry<string>;
Onyx.connect({

Check warning on line 887 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.CONCIERGE_REPORT_ID,
callback: (value) => {
conciergeReportID = value;
Expand All @@ -892,7 +892,7 @@
});

const defaultAvatarBuildingIconTestID = 'SvgDefaultAvatarBuilding Icon';
Onyx.connect({

Check warning on line 895 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.SESSION,
callback: (value) => {
// When signed out, val is undefined
Expand All @@ -910,7 +910,7 @@
let allPersonalDetails: OnyxEntry<PersonalDetailsList>;
let allPersonalDetailLogins: string[];
let currentUserPersonalDetails: OnyxEntry<PersonalDetails>;
Onyx.connect({

Check warning on line 913 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.PERSONAL_DETAILS_LIST,
callback: (value) => {
if (currentUserAccountID) {
Expand All @@ -922,14 +922,14 @@
});

let allReportsDraft: OnyxCollection<Report>;
Onyx.connect({

Check warning on line 925 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_DRAFT,
waitForCollectionCallback: true,
callback: (value) => (allReportsDraft = value),
});

let allPolicies: OnyxCollection<Policy>;
Onyx.connect({

Check warning on line 932 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.POLICY,
waitForCollectionCallback: true,
callback: (value) => (allPolicies = value),
Expand All @@ -937,7 +937,7 @@

let allReports: OnyxCollection<Report>;
let reportsByPolicyID: ReportByPolicyMap;
Onyx.connect({

Check warning on line 940 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -974,14 +974,14 @@
});

let allBetas: OnyxEntry<Beta[]>;
Onyx.connect({

Check warning on line 977 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.BETAS,
callback: (value) => (allBetas = value),
});

let allTransactions: OnyxCollection<Transaction> = {};
let reportsTransactions: Record<string, Transaction[]> = {};
Onyx.connect({

Check warning on line 984 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.TRANSACTION,
waitForCollectionCallback: true,
callback: (value) => {
Expand All @@ -1007,7 +1007,7 @@
});

let allReportActions: OnyxCollection<ReportActions>;
Onyx.connect({

Check warning on line 1010 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_ACTIONS,
waitForCollectionCallback: true,
callback: (actions) => {
Expand All @@ -1020,7 +1020,7 @@

let allReportMetadata: OnyxCollection<ReportMetadata>;
const allReportMetadataKeyValue: Record<string, ReportMetadata> = {};
Onyx.connect({

Check warning on line 1023 in src/libs/ReportUtils.ts

View workflow job for this annotation

GitHub Actions / Changed files ESLint check

Onyx.connect() is deprecated. Use useOnyx() hook instead and pass the data as parameters to a pure function
key: ONYXKEYS.COLLECTION.REPORT_METADATA,
waitForCollectionCallback: true,
callback: (value) => {
Expand Down Expand Up @@ -8379,13 +8379,6 @@
* - It's an ADD_COMMENT that is not an attachment
*/
function canFlagReportAction(reportAction: OnyxInputOrEntry<ReportAction>, reportID: string | undefined): boolean {
let report = getReportOrDraftReport(reportID);

// If the childReportID exists in reportAction and is equal to the reportID,
// the report action being evaluated is the parent report action in a thread, and we should get the parent report to evaluate instead.
if (reportAction?.childReportID?.toString() === reportID?.toString()) {
report = getReportOrDraftReport(report?.parentReportID);
}
const isCurrentUserAction = reportAction?.actorAccountID === currentUserAccountID;
if (isWhisperAction(reportAction)) {
// Allow flagging whispers that are sent by other users
Expand All @@ -8397,6 +8390,14 @@
return false;
}

let report = getReportOrDraftReport(reportID);

// If the childReportID exists in reportAction and is equal to the reportID,
// the report action being evaluated is the parent report action in a thread, and we should get the parent report to evaluate instead.
if (reportAction?.childReportID?.toString() === reportID?.toString()) {
report = getReportOrDraftReport(report?.parentReportID);
}

return !!(
!isCurrentUserAction &&
reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT &&
Expand All @@ -8411,12 +8412,10 @@
/**
* Whether flag comment page should show
*/
function shouldShowFlagComment(reportAction: OnyxInputOrEntry<ReportAction>, report: OnyxInputOrEntry<Report>): boolean {
function shouldShowFlagComment(reportAction: OnyxInputOrEntry<ReportAction>, report: OnyxInputOrEntry<Report>, isReportArchived = false): boolean {
return (
canFlagReportAction(reportAction, report?.reportID) &&
// This will get removed as part of https://github.com/Expensify/App/issues/59961
// eslint-disable-next-line deprecation/deprecation
!isArchivedNonExpenseReport(report, !!getReportNameValuePairs(report?.reportID)?.private_isArchived) &&
!isArchivedNonExpenseReport(report, isReportArchived) &&
!chatIncludesChronos(report) &&
!isConciergeChatReport(report) &&
reportAction?.actorAccountID !== CONST.ACCOUNT_ID.CONCIERGE
Expand Down
4 changes: 3 additions & 1 deletion src/pages/FlagCommentPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import ScreenWrapper from '@components/ScreenWrapper';
import ScrollView from '@components/ScrollView';
import Text from '@components/Text';
import useLocalize from '@hooks/useLocalize';
import useReportIsArchived from '@hooks/useReportIsArchived';
import useThemeStyles from '@hooks/useThemeStyles';
import Navigation from '@libs/Navigation/Navigation';
import type {PlatformStackScreenProps} from '@libs/Navigation/PlatformStackNavigation/types';
Expand Down Expand Up @@ -49,6 +50,7 @@ function getReportID(route: FlagCommentPageNavigationProps['route']) {
function FlagCommentPage({parentReportAction, route, report, parentReport, reportAction}: FlagCommentPageProps) {
const styles = useThemeStyles();
const {translate} = useLocalize();
const isReportArchived = useReportIsArchived(report?.reportID);

const severities: SeverityItemList = [
{
Expand Down Expand Up @@ -135,7 +137,7 @@ function FlagCommentPage({parentReportAction, route, report, parentReport, repor
testID={FlagCommentPage.displayName}
>
{({safeAreaPaddingBottomStyle}) => (
<FullPageNotFoundView shouldShow={!shouldShowFlagComment(reportAction, report)}>
<FullPageNotFoundView shouldShow={!shouldShowFlagComment(reportAction, report, isReportArchived)}>
<HeaderWithBackButton
title={translate('reportActionContextMenu.flagAsOffensive')}
onBackButtonPress={() => Navigation.goBack(route.params.backTo)}
Expand Down
281 changes: 278 additions & 3 deletions tests/unit/ReportUtilsTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {putOnHold} from '@libs/actions/IOU';
import type {OnboardingTaskLinks} from '@libs/actions/Welcome/OnboardingFlow';
import DateUtils from '@libs/DateUtils';
import {translateLocal} from '@libs/Localize';
import {getOriginalMessage} from '@libs/ReportActionsUtils';
import {getOriginalMessage, isWhisperAction} from '@libs/ReportActionsUtils';
import {
buildOptimisticChatReport,
buildOptimisticCreatedReportAction,
Expand All @@ -25,6 +25,7 @@ import {
canEditReportDescription,
canEditRoomVisibility,
canEditWriteCapability,
canFlagReportAction,
canHoldUnholdReportAction,
findLastAccessedReport,
getAllAncestorReportActions,
Expand Down Expand Up @@ -59,6 +60,7 @@ import {
shouldDisableThread,
shouldReportBeInOptionList,
shouldReportShowSubscript,
shouldShowFlagComment,
temporary_getMoneyRequestOptions,
} from '@libs/ReportUtils';
import type {OptionData} from '@libs/ReportUtils';
Expand All @@ -74,7 +76,7 @@ import {chatReportR14932 as mockedChatReport} from '../../__mocks__/reportData/r
import * as NumberUtils from '../../src/libs/NumberUtils';
import {convertedInvoiceChat} from '../data/Invoice';
import createRandomPolicy from '../utils/collections/policies';
import createRandomReportAction from '../utils/collections/reportActions';
import createRandomReportAction, {getRandomDate} from '../utils/collections/reportActions';
import {
createAdminRoom,
createAnnounceRoom,
Expand Down Expand Up @@ -2258,7 +2260,7 @@ describe('ReportUtils', () => {
).toBeFalsy();
});

it('should return false when the user’s email is domain-based and the includeDomainEmail is false', () => {
it('should return false when the users email is domain-based and the includeDomainEmail is false', () => {
const report = LHNTestUtils.getFakeReport();
const currentReportId = '';
const isInFocusMode = false;
Expand Down Expand Up @@ -3919,4 +3921,277 @@ describe('ReportUtils', () => {
expect(result).toBe(false);
});
});

describe('isWhisperAction', () => {
it('an action where reportAction.message.whisperedTo has accountIDs is a whisper action', () => {
const whisperReportAction: ReportAction = {
...createRandomReportAction(1),
};
expect(isWhisperAction(whisperReportAction)).toBe(true);
});
it('an action where reportAction.originalMessage.whisperedTo does not exist is not a whisper action', () => {
const nonWhisperReportAction = {
...createRandomReportAction(1),
message: [
{
whisperedTo: undefined,
},
],
} as ReportAction;
expect(isWhisperAction(nonWhisperReportAction)).toBe(false);
});
});

describe('canFlagReportAction', () => {
describe('a whisper action', () => {
const whisperReportAction: ReportAction = {
...createRandomReportAction(1),
};

it('cannot be flagged if it is from concierge', () => {
const whisperReportActionFromConcierge = {
...whisperReportAction,
actorAccountID: CONST.ACCOUNT_ID.CONCIERGE,
};

// The reportID doesn't matter because there is an early return for whisper actions and the report is not looked at
expect(canFlagReportAction(whisperReportActionFromConcierge, '123456')).toBe(false);
});

it('cannot be flagged if it is from the current user', () => {
const whisperReportActionFromCurrentUser = {
...whisperReportAction,
actorAccountID: currentUserAccountID,
};

// The reportID doesn't matter because there is an early return for whisper actions and the report is not looked at
expect(canFlagReportAction(whisperReportActionFromCurrentUser, '123456')).toBe(false);
});

it('can be flagged if it is not from concierge or the current user', () => {
expect(canFlagReportAction(whisperReportAction, '123456')).toBe(true);
});
});

describe('a non-whisper action', () => {
const report = {
...createRandomReport(1),
};
const nonWhisperReportAction = {
...createRandomReportAction(1),
actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
message: [
{
whisperedTo: undefined,
},
],
} as ReportAction;

beforeAll(async () => {
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report);
});

afterAll(async () => {
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, null);
});

it('cannot be flagged if it is from the current user', () => {
const nonWhisperReportActionFromCurrentUser = {
...nonWhisperReportAction,
actorAccountID: currentUserAccountID,
};
expect(canFlagReportAction(nonWhisperReportActionFromCurrentUser, report.reportID)).toBe(false);
});

it('cannot be flagged if the action name is something other than ADD_COMMENT', () => {
const nonWhisperReportActionWithDifferentActionName = {
...nonWhisperReportAction,
actionName: CONST.REPORT.ACTIONS.TYPE.APPROVED,
};
expect(canFlagReportAction(nonWhisperReportActionWithDifferentActionName, report.reportID)).toBe(false);
});

it('cannot be flagged if the action is deleted', () => {
const deletedReportAction = {
...nonWhisperReportAction,
message: [
{
whisperedTo: undefined,
html: '',
deleted: getRandomDate(),
},
],
} as ReportAction;
expect(canFlagReportAction(deletedReportAction, report.reportID)).toBe(false);
});

it('cannot be flagged if the action is a created task report', () => {
const createdTaskReportAction = {
...nonWhisperReportAction,
originalMessage: {
// This signifies that the action is a created task report along with the ADD_COMMENT action name
taskReportID: '123456',
},
} as ReportAction;
expect(canFlagReportAction(createdTaskReportAction, report.reportID)).toBe(false);
});

it('cannot be flagged if the report does not exist', () => {
// cspell:disable-next-line
expect(canFlagReportAction(nonWhisperReportAction, 'starwarsisthebest')).toBe(false);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
expect(canFlagReportAction(nonWhisperReportAction, 'starwarsisthebest')).toBe(false);
expect(canFlagReportAction(nonWhisperReportAction, '1234')).toBe(false);

});

it('cannot be flagged if the report is not allowed to be commented on', () => {
// eslint-disable-next-line rulesdir/no-negated-variables
const reportThatCannotBeCommentedOn = {
...createRandomReport(2),

// If the permissions does not contain WRITE, then it cannot be commented on
permissions: [],
} as Report;
expect(canFlagReportAction(nonWhisperReportAction, reportThatCannotBeCommentedOn.reportID)).toBe(false);
});

it('can be flagged', () => {
expect(canFlagReportAction(nonWhisperReportAction, report.reportID)).toBe(true);
});
});
});

// Note: shouldShowFlagComment() calls isArchivedNonExpenseReport() which has it's own unit tests, so whether
// the report is an expense report or not does not need to be tested here.
describe('shouldShowFlagComment', () => {
const validReportAction: ReportAction = {
...createRandomReportAction(1),
actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,

// Actor is not the current user or Concierge
actorAccountID: 123456,
};

describe('can flag report action', () => {
let expenseReport: Report;
const reportActionThatCanBeFlagged: ReportAction = {
...validReportAction,
};

// eslint-disable-next-line rulesdir/no-negated-variables
const reportActionThatCannotBeFlagged: ReportAction = {
...createRandomReportAction(1),
actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,

// If the actor is Concierge, the report action cannot be flagged
actorAccountID: CONST.ACCOUNT_ID.CONCIERGE,
};

beforeAll(async () => {
expenseReport = {
...createRandomReport(60000),
type: CONST.REPORT.TYPE.EXPENSE,
};
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, expenseReport);
});

afterAll(async () => {
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${expenseReport.reportID}`, null);
});

it('should return true for an archived expense report with an action that can be flagged', () => {
expect(shouldShowFlagComment(reportActionThatCanBeFlagged, expenseReport, true)).toBe(true);
});

it('should return true for a non-archived expense report with an action that can be flagged', () => {
expect(shouldShowFlagComment(reportActionThatCanBeFlagged, expenseReport, false)).toBe(true);
});

it('should return false for an archived expense report with an action that cannot be flagged', () => {
expect(shouldShowFlagComment(reportActionThatCannotBeFlagged, expenseReport, true)).toBe(false);
});

it('should return false for a non-archived expense report with an action that cannot be flagged', () => {
expect(shouldShowFlagComment(reportActionThatCannotBeFlagged, expenseReport, false)).toBe(false);
});
});

describe('Chat with Chronos', () => {
let chatReport: Report;

beforeAll(async () => {
chatReport = {
...createRandomReport(60000),
type: CONST.REPORT.TYPE.CHAT,
participants: buildParticipantsFromAccountIDs([currentUserAccountID, CONST.ACCOUNT_ID.CHRONOS]),
};
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport);
});

afterAll(async () => {
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, null);
});

it('should return false for an archived chat report', () => {
expect(shouldShowFlagComment(validReportAction, chatReport, true)).toBe(false);
});

it('should return false for a non-archived chat report', () => {
expect(shouldShowFlagComment(validReportAction, chatReport, false)).toBe(false);
});
});

describe('Chat with Concierge', () => {
let chatReport: Report;

beforeAll(async () => {
chatReport = {
...createRandomReport(60000),
type: CONST.REPORT.TYPE.CHAT,
participants: buildParticipantsFromAccountIDs([currentUserAccountID, CONST.ACCOUNT_ID.CONCIERGE]),
};
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport);
await Onyx.set(`${ONYXKEYS.CONCIERGE_REPORT_ID}`, chatReport.reportID);
});

afterAll(async () => {
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, null);
await Onyx.set(`${ONYXKEYS.CONCIERGE_REPORT_ID}`, null);
});

it('should return false for an archived chat report', () => {
expect(shouldShowFlagComment(validReportAction, chatReport, true)).toBe(false);
});

it('should return false for a non-archived chat report', () => {
expect(shouldShowFlagComment(validReportAction, chatReport, false)).toBe(false);
});
});

describe('Action from Concierge', () => {
let chatReport: Report;
const actionFromConcierge: ReportAction = {
...createRandomReportAction(1),
actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT,
actorAccountID: CONST.ACCOUNT_ID.CONCIERGE,
};

beforeAll(async () => {
chatReport = {
...createRandomReport(60000),
type: CONST.REPORT.TYPE.CHAT,
};
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, chatReport);
});

afterAll(async () => {
await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReport.reportID}`, null);
});

it('should return false for an archived chat report', () => {
expect(shouldShowFlagComment(actionFromConcierge, chatReport, true)).toBe(false);
});

it('should return false for a non-archived chat report', () => {
expect(shouldShowFlagComment(actionFromConcierge, chatReport, false)).toBe(false);
});
});
});
});
Loading