Skip to content
Draft
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
2 changes: 2 additions & 0 deletions src/ONYXKEYS.ts
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,7 @@ const ONYXKEYS = {
REPORT_ATTRIBUTES: 'reportAttributes',
REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations',
OUTSTANDING_REPORTS_BY_POLICY_ID: 'outstandingReportsByPolicyID',
VISIBLE_REPORT_ACTIONS: 'visibleReportActions',
},

/** Stores HybridApp specific state required to interoperate with OldDot */
Expand Down Expand Up @@ -1381,6 +1382,7 @@ type OnyxDerivedValuesMapping = {
[ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: OnyxTypes.ReportAttributesDerivedValue;
[ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: OnyxTypes.ReportTransactionsAndViolationsDerivedValue;
[ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: OnyxTypes.OutstandingReportsByPolicyIDDerivedValue;
[ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS]: OnyxTypes.VisibleReportActionsDerivedValue;
};

type OnyxValues = OnyxValuesMapping & OnyxCollectionValuesMapping & OnyxFormValuesMapping & OnyxFormDraftValuesMapping & OnyxDerivedValuesMapping;
Expand Down
3 changes: 3 additions & 0 deletions src/libs/ReportActionsUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@
type MemberChangeMessageElement = MessageTextElement | MemberChangeMessageUserMentionElement | MemberChangeMessageRoomReferenceElement;

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

Check warning on line 63 in src/libs/ReportActionsUtils.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 @@ -72,7 +72,7 @@
});

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

Check warning on line 75 in src/libs/ReportActionsUtils.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 All @@ -81,14 +81,14 @@
});

let isNetworkOffline = false;
Onyx.connect({

Check warning on line 84 in src/libs/ReportActionsUtils.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.NETWORK,
callback: (val) => (isNetworkOffline = val?.isOffline ?? false),
});

let currentUserAccountID: number | undefined;
let currentEmail = '';
Onyx.connect({

Check warning on line 91 in src/libs/ReportActionsUtils.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, value is undefined
Expand Down Expand Up @@ -3761,6 +3761,9 @@
isSystemUserMentioned,
withDEWRoutedActionsArray,
withDEWRoutedActionsObject,
isTravelUpdate,
isVisiblePreviewOrMoneyRequest,
isActionableJoinRequestPendingReportAction,
};

export type {LastVisibleMessage};
2 changes: 2 additions & 0 deletions src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import ONYXKEYS from '@src/ONYXKEYS';
import outstandingReportsByPolicyIDConfig from './configs/outstandingReportsByPolicyID';
import reportAttributesConfig from './configs/reportAttributes';
import reportTransactionsAndViolationsConfig from './configs/reportTransactionsAndViolations';
import visibleReportActionsConfig from './configs/visibleReportActions';
import type {OnyxDerivedValueConfig} from './types';

/**
Expand All @@ -13,6 +14,7 @@ const ONYX_DERIVED_VALUES = {
[ONYXKEYS.DERIVED.REPORT_ATTRIBUTES]: reportAttributesConfig,
[ONYXKEYS.DERIVED.REPORT_TRANSACTIONS_AND_VIOLATIONS]: reportTransactionsAndViolationsConfig,
[ONYXKEYS.DERIVED.OUTSTANDING_REPORTS_BY_POLICY_ID]: outstandingReportsByPolicyIDConfig,
[ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS]: visibleReportActionsConfig,
} as const satisfies {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[Key in ValueOf<typeof ONYXKEYS.DERIVED>]: OnyxDerivedValueConfig<Key, any>;
Expand Down
249 changes: 249 additions & 0 deletions src/libs/actions/OnyxDerived/configs/visibleReportActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
import type {OnyxCollection, OnyxEntry} from 'react-native-onyx';
import {
getOriginalMessage,
getWhisperedTo,
isActionableCardFraudAlert,
isActionableJoinRequestPendingReportAction,
isActionableMentionWhisper,
isActionableReportMentionWhisper,
isActionableWhisper,
isDeletedAction,
isDeletedParentAction,
isMarkAsClosedAction,
isMovedTransactionAction,
isPendingRemove,
isReportActionDeprecated,
isResolvedActionableWhisper,
isReversedTransaction,
isTravelUpdate,
isTripPreview,
isVisiblePreviewOrMoneyRequest,
isWhisperAction,
} from '@libs/ReportActionsUtils';
import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig';
import CONST from '@src/CONST';
import ONYXKEYS from '@src/ONYXKEYS';
import type {Report, ReportAction, ReportActions} from '@src/types/onyx';
import type {VisibleReportActionsDerivedValue} from '@src/types/onyx/DerivedValues';
import type {OriginalMessageMovedTransaction, OriginalMessageUnreportedTransaction} from '@src/types/onyx/OriginalMessage';
import type ReportActionName from '@src/types/onyx/ReportActionName';

const {POLICY_CHANGE_LOG: policyChangelogTypes, ROOM_CHANGE_LOG: roomChangeLogTypes, ...otherActionTypes} = CONST.REPORT.ACTIONS.TYPE;
const supportedActionTypes = new Set<ReportActionName>([...Object.values(otherActionTypes), ...Object.values(policyChangelogTypes), ...Object.values(roomChangeLogTypes)]);

function getOrCreateReportVisibilityRecord(result: VisibleReportActionsDerivedValue, reportID: string): Record<string, boolean> {
if (!result[reportID]) {
// eslint-disable-next-line no-param-reassign
result[reportID] = {};
}
return result[reportID];
}

function isUnreportedTransactionVisible(reportAction: ReportAction, allReports: OnyxCollection<Report>): boolean {
const originalMessage = getOriginalMessage(reportAction) as OriginalMessageUnreportedTransaction | undefined;

if (!originalMessage?.fromReportID) {
return false;
}

const fromReportKey = `${ONYXKEYS.COLLECTION.REPORT}${originalMessage.fromReportID}`;
const fromReport = allReports?.[fromReportKey];

return !!fromReport;
}

function isMovedTransactionVisible(reportAction: ReportAction, allReports: OnyxCollection<Report>): boolean {
const originalMessage = getOriginalMessage(reportAction) as OriginalMessageMovedTransaction | undefined;

if (!originalMessage) {
return false;
}

const toReportID = originalMessage.toReportID;
const fromReportID = originalMessage.fromReportID;

const isFromUnreportedReport = fromReportID === CONST.REPORT.UNREPORTED_REPORT_ID;

const toReportKey = `${ONYXKEYS.COLLECTION.REPORT}${toReportID}`;
const fromReportKey = `${ONYXKEYS.COLLECTION.REPORT}${fromReportID}`;

const toReportExists = !!allReports?.[toReportKey];
const fromReportExists = isFromUnreportedReport || !!allReports?.[fromReportKey];

return fromReportExists || toReportExists;
}

function doesActionDependOnReportExistence(action: ReportAction): boolean {
const isUnreportedTransaction = action.actionName === CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION;
const isMovedTransaction = isMovedTransactionAction(action as OnyxEntry<ReportAction>);

return isUnreportedTransaction || isMovedTransaction;
}

function isReportActionStaticallyVisible(reportAction: OnyxEntry<ReportAction>, key: string | number, allReports: OnyxCollection<Report>, currentUserAccountID: number | undefined): boolean {
if (!reportAction) {
return false;
}

if (isReportActionDeprecated(reportAction, key)) {
return false;
}

if (!supportedActionTypes.has(reportAction.actionName)) {
return false;
}

if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION) {
return isUnreportedTransactionVisible(reportAction, allReports);
}

if (isMovedTransactionAction(reportAction)) {
return isMovedTransactionVisible(reportAction, allReports);
}

if (reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED) {
const isMarkAsClosed = isMarkAsClosedAction(reportAction);
if (!isMarkAsClosed) {
return false;
}
}

if (isWhisperAction(reportAction)) {
const whisperedToAccountIDs = getWhisperedTo(reportAction);
const isWhisperTargetedToCurrentUser = whisperedToAccountIDs.includes(currentUserAccountID ?? CONST.DEFAULT_NUMBER_ID);
if (!isWhisperTargetedToCurrentUser) {
return false;
}
}

if (isPendingRemove(reportAction) && !reportAction.childVisibleActionCount) {
return false;
}

if (isTripPreview(reportAction) || isTravelUpdate(reportAction)) {
return true;
}

if (isActionableWhisper(reportAction) && isResolvedActionableWhisper(reportAction)) {
return false;
}

if (!isVisiblePreviewOrMoneyRequest(reportAction)) {
return false;
}

const isDeleted = isDeletedAction(reportAction);
const isPending = !!reportAction.pendingAction;
const isParentAction = isDeletedParentAction(reportAction);
const isReversed = isReversedTransaction(reportAction);

return !isDeleted || isPending || isParentAction || isReversed;
}

function isActionableWhisperRequiringWritePermission(reportAction: OnyxEntry<ReportAction>): boolean {
if (!reportAction) {
return false;
}

const isReportMentionWhisper = isActionableReportMentionWhisper(reportAction);
const isJoinRequestPending = isActionableJoinRequestPendingReportAction(reportAction);
const isMentionWhisper = isActionableMentionWhisper(reportAction);
const isCardFraudAlert = isActionableCardFraudAlert(reportAction);

return isReportMentionWhisper || isJoinRequestPending || isMentionWhisper || isCardFraudAlert;
}

export {isActionableWhisperRequiringWritePermission};

export default createOnyxDerivedValueConfig({
key: ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS,
dependencies: [ONYXKEYS.COLLECTION.REPORT_ACTIONS, ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.SESSION],
compute: ([allReportActions, allReports, session], {sourceValues, currentValue}): VisibleReportActionsDerivedValue => {
if (!allReportActions) {
return {};
}

const currentUserAccountID = session?.accountID;

const reportActionsUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT_ACTIONS];
const reportUpdates = sourceValues?.[ONYXKEYS.COLLECTION.REPORT];
const sessionUpdates = sourceValues?.[ONYXKEYS.SESSION];

// Session change = user changed, need full recompute due to whisper targeting
if (sessionUpdates) {
const result: VisibleReportActionsDerivedValue = {};

for (const [reportActionsKey, reportActions] of Object.entries(allReportActions)) {
if (!reportActions) {
continue;
}

const reportID = reportActionsKey.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, '');
const reportVisibility = getOrCreateReportVisibilityRecord(result, reportID);

for (const [actionID, action] of Object.entries(reportActions)) {
if (action) {
reportVisibility[actionID] = isReportActionStaticallyVisible(action, actionID, allReports, currentUserAccountID);
}
}
}

return result;
}

// Only reports changed - recompute actions that depend on report existence
if (reportUpdates && !reportActionsUpdates) {
const result: VisibleReportActionsDerivedValue = currentValue ? {...currentValue} : {};

for (const [reportActionsKey, reportActions] of Object.entries(allReportActions)) {
if (!reportActions) {
continue;
}

const reportID = reportActionsKey.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, '');
const reportVisibility = getOrCreateReportVisibilityRecord(result, reportID);

for (const [actionID, action] of Object.entries(reportActions)) {
if (!action) {
continue;
}

if (doesActionDependOnReportExistence(action)) {
reportVisibility[actionID] = isReportActionStaticallyVisible(action, actionID, allReports, currentUserAccountID);
}
}
}

return result;
}

const result: VisibleReportActionsDerivedValue = currentValue ? {...currentValue} : {};
const reportActionsToProcess = reportActionsUpdates ? Object.keys(reportActionsUpdates) : Object.keys(allReportActions);

for (const reportActionsKey of reportActionsToProcess) {
const reportActions: OnyxEntry<ReportActions> = allReportActions[reportActionsKey];
const reportID = reportActionsKey.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, '');

if (!reportActions) {
delete result[reportID];
continue;
}

const reportVisibility = getOrCreateReportVisibilityRecord(result, reportID);

const specificUpdates = reportActionsUpdates?.[reportActionsKey];
const actionsToProcess = specificUpdates ? Object.entries(specificUpdates) : Object.entries(reportActions);

for (const [actionID, action] of actionsToProcess) {
if (!action) {
delete reportVisibility[actionID];
continue;
}

reportVisibility[actionID] = isReportActionStaticallyVisible(action, actionID, allReports, currentUserAccountID);
}
}

return result;
},
});
37 changes: 29 additions & 8 deletions src/pages/home/report/ReportActionsView.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import useReportIsArchived from '@hooks/useReportIsArchived';
import useResponsiveLayout from '@hooks/useResponsiveLayout';
import useTransactionsAndViolationsForReport from '@hooks/useTransactionsAndViolationsForReport';
import {getReportPreviewAction} from '@libs/actions/IOU';
import {isActionableWhisperRequiringWritePermission} from '@libs/actions/OnyxDerived/configs/visibleReportActions';
import {updateLoadingInitialReportAction} from '@libs/actions/Report';
import DateUtils from '@libs/DateUtils';
import getIsReportFullyVisible from '@libs/getIsReportFullyVisible';
Expand All @@ -29,7 +30,6 @@ import {
isDeletedParentAction,
isIOUActionMatchingTransactionList,
isMoneyRequestAction,
shouldReportActionBeVisible,
} from '@libs/ReportActionsUtils';
import {buildOptimisticCreatedReportAction, buildOptimisticIOUReportAction, canUserPerformWriteAction, isInvoiceReport, isMoneyRequestReport} from '@libs/ReportUtils';
import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd';
Expand Down Expand Up @@ -102,6 +102,7 @@ function ReportActionsView({
);
const [transactionThreadReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${transactionThreadReportID}`, {canBeMissing: true});
const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true});
const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true});
const prevTransactionThreadReport = usePrevious(transactionThreadReport);
const reportActionID = route?.params?.reportActionID;
const prevReportActionID = usePrevious(reportActionID);
Expand Down Expand Up @@ -219,13 +220,33 @@ function ReportActionsView({

const visibleReportActions = useMemo(
() =>
reportActions.filter(
(reportAction) =>
(isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) &&
shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) &&
isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs),
),
[reportActions, isOffline, canPerformWriteAction, reportTransactionIDs],
reportActions.filter((reportAction) => {
const passesOfflineCheck =
isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors;

if (!passesOfflineCheck) {
return false;
}

const actionReportID = reportAction.reportID ?? reportID;
const isStaticallyVisible = visibleReportActionsData?.[actionReportID]?.[reportAction.reportActionID];

const passesStaticVisibility = isStaticallyVisible ?? true;
if (!passesStaticVisibility) {
return false;
}

if (!canPerformWriteAction && isActionableWhisperRequiringWritePermission(reportAction)) {
return false;
}

if (!isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)) {
return false;
}

return true;
}),
[reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, visibleReportActionsData, reportID],
);

const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]);
Expand Down
14 changes: 13 additions & 1 deletion src/types/onyx/DerivedValues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,5 +70,17 @@ type ReportTransactionsAndViolationsDerivedValue = Record<string, ReportTransact
*/
type OutstandingReportsByPolicyIDDerivedValue = Record<string, OnyxCollection<Report>>;

/**
* The derived value for visible report actions.
*/
type VisibleReportActionsDerivedValue = Record<string, Record<string, boolean>>;

export default ReportAttributesDerivedValue;
export type {ReportAttributes, ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerivedValue, ReportTransactionsAndViolations, OutstandingReportsByPolicyIDDerivedValue};
export type {
ReportAttributes,
ReportAttributesDerivedValue,
ReportTransactionsAndViolationsDerivedValue,
ReportTransactionsAndViolations,
OutstandingReportsByPolicyIDDerivedValue,
VisibleReportActionsDerivedValue,
};
3 changes: 2 additions & 1 deletion src/types/onyx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ import type Credentials from './Credentials';
import type Currency from './Currency';
import type {CurrencyList} from './Currency';
import type CustomStatusDraft from './CustomStatusDraft';
import type {OutstandingReportsByPolicyIDDerivedValue, ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerivedValue} from './DerivedValues';
import type {OutstandingReportsByPolicyIDDerivedValue, ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerivedValue, VisibleReportActionsDerivedValue} from './DerivedValues';
import type DismissedProductTraining from './DismissedProductTraining';
import type DismissedReferralBanners from './DismissedReferralBanners';
import type Domain from './Domain';
Expand Down Expand Up @@ -301,6 +301,7 @@ export type {
LastSearchParams,
ReportTransactionsAndViolationsDerivedValue,
OutstandingReportsByPolicyIDDerivedValue,
VisibleReportActionsDerivedValue,
ScheduleCallDraft,
ValidateUserAndGetAccessiblePolicies,
VacationDelegate,
Expand Down
Loading