diff --git a/src/ONYXKEYS.ts b/src/ONYXKEYS.ts index 2edd44ea9550..e9e01732ccf4 100755 --- a/src/ONYXKEYS.ts +++ b/src/ONYXKEYS.ts @@ -997,6 +997,7 @@ const ONYXKEYS = { REPORT_ATTRIBUTES: 'reportAttributes', REPORT_TRANSACTIONS_AND_VIOLATIONS: 'reportTransactionsAndViolations', OUTSTANDING_REPORTS_BY_POLICY_ID: 'outstandingReportsByPolicyID', + VISIBLE_REPORT_ACTIONS: 'visibleReportActions', NON_PERSONAL_AND_WORKSPACE_CARD_LIST: 'nonPersonalAndWorkspaceCardList', CARD_FEED_ERRORS: 'cardFeedErrors', }, @@ -1413,6 +1414,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; [ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST]: OnyxTypes.NonPersonalAndWorkspaceCardListDerivedValue; [ONYXKEYS.DERIVED.CARD_FEED_ERRORS]: OnyxTypes.CardFeedErrorsDerivedValue; }; diff --git a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts index 6dffd994d6a7..df0d08946c19 100644 --- a/src/components/Attachments/AttachmentCarousel/extractAttachments.ts +++ b/src/components/Attachments/AttachmentCarousel/extractAttachments.ts @@ -3,11 +3,11 @@ import type {OnyxEntry} from 'react-native-onyx'; import type {ValueOf} from 'type-fest'; import type {Attachment} from '@components/Attachments/types'; import {getFileName, splitExtensionFromFileName} from '@libs/fileDownload/FileUtils'; -import {getHtmlWithAttachmentID, getReportActionHtml, getReportActionMessage, getSortedReportActions, isMoneyRequestAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; +import {getHtmlWithAttachmentID, getReportActionHtml, getReportActionMessage, getSortedReportActions, isMoneyRequestAction, isReportActionVisible} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction} from '@libs/ReportUtils'; import tryResolveUrlFromApiRoot from '@libs/tryResolveUrlFromApiRoot'; import CONST from '@src/CONST'; -import type {Report, ReportAction, ReportActions} from '@src/types/onyx'; +import type {Report, ReportAction, ReportActions, VisibleReportActionsDerivedValue} from '@src/types/onyx'; import type {Note} from '@src/types/onyx/Report'; /** @@ -22,6 +22,7 @@ function extractAttachments( reportActions, report, isReportArchived, + visibleReportActionsData, }: { privateNotes?: Record; accountID?: number; @@ -29,6 +30,7 @@ function extractAttachments( reportActions?: OnyxEntry; report: OnyxEntry; isReportArchived: boolean | undefined; + visibleReportActionsData?: VisibleReportActionsDerivedValue; }, ) { const targetNote = privateNotes?.[Number(accountID)]?.note ?? ''; @@ -115,9 +117,13 @@ function extractAttachments( return attachments.reverse(); } + const reportID = report?.reportID; + if (!reportID) { + return attachments.reverse(); + } const actions = [...(parentReportAction ? [parentReportAction] : []), ...getSortedReportActions(Object.values(reportActions ?? {}))]; - for (const [key, action] of actions.entries()) { - if (!shouldReportActionBeVisible(action, key, canUserPerformAction) || isMoneyRequestAction(action)) { + for (const action of actions) { + if (!isReportActionVisible(action, reportID, canUserPerformAction, visibleReportActionsData) || isMoneyRequestAction(action)) { continue; } diff --git a/src/components/LHNOptionsList/LHNOptionsList.tsx b/src/components/LHNOptionsList/LHNOptionsList.tsx index 41047755eb59..4572b1e35d79 100644 --- a/src/components/LHNOptionsList/LHNOptionsList.tsx +++ b/src/components/LHNOptionsList/LHNOptionsList.tsx @@ -29,20 +29,20 @@ import Log from '@libs/Log'; import {getMovedReportID} from '@libs/ModifiedExpenseMessage'; import {getIOUReportIDOfLastAction, getLastMessageTextForReport} from '@libs/OptionsListUtils'; import { + getLastVisibleAction, getOneTransactionThreadReportID, getOriginalMessage, - getSortedReportActions, - getSortedReportActionsForDisplay, + getReportActionActorAccountID, isInviteOrRemovedAction, isMoneyRequestAction, - shouldReportActionBeVisibleAsLastAction, + isReportPreviewAction, } from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionUtil} from '@libs/ReportUtils'; import variables from '@styles/variables'; import CONST from '@src/CONST'; import NAVIGATORS from '@src/NAVIGATORS'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {PersonalDetails, Report, ReportAction} from '@src/types/onyx'; +import type {PersonalDetails, Report} from '@src/types/onyx'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; import OptionRowLHNData from './OptionRowLHNData'; import OptionRowRendererComponent from './OptionRowRendererComponent'; @@ -73,6 +73,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const [introSelected] = useOnyx(ONYXKEYS.NVP_INTRO_SELECTED, {canBeMissing: true}); const [onboarding] = useOnyx(ONYXKEYS.NVP_ONBOARDING, {canBeMissing: true}); const [isFullscreenVisible] = useOnyx(ONYXKEYS.FULLSCREEN_VISIBILITY, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const {accountID: currentUserAccountID} = useCurrentUserPersonalDetails(); const {policyForMovingExpensesID} = usePolicyForMovingExpenses(); @@ -193,9 +194,6 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio } const itemInvoiceReceiverPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${invoiceReceiverPolicyID}`]; - const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item); - const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined; - const itemPolicy = policy?.[`${ONYXKEYS.COLLECTION.POLICY}${item?.policyID}`]; const transactionID = isMoneyRequestAction(itemParentReportAction) ? (getOriginalMessage(itemParentReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) @@ -207,55 +205,56 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio const isReportArchived = !!itemReportNameValuePairs?.private_isArchived; const canUserPerformWrite = canUserPerformWriteActionUtil(item, isReportArchived); - const sortedReportActions = getSortedReportActionsForDisplay(itemReportActions, canUserPerformWrite); - const lastReportAction = sortedReportActions.at(0); + const lastAction = getLastVisibleAction( + reportID, + canUserPerformWrite, + {}, + itemReportActions ? {[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`]: itemReportActions} : undefined, + visibleReportActionsData, + ); - // Get the transaction for the last report action - const lastReportActionTransactionID = isMoneyRequestAction(lastReportAction) - ? (getOriginalMessage(lastReportAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) - : CONST.DEFAULT_NUMBER_ID; + const iouReportIDOfLastAction = getIOUReportIDOfLastAction(item, visibleReportActionsData, lastAction); + const itemIouReportReportActions = iouReportIDOfLastAction ? reportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportIDOfLastAction}`] : undefined; + + const lastReportActionTransactionID = isMoneyRequestAction(lastAction) ? (getOriginalMessage(lastAction)?.IOUTransactionID ?? CONST.DEFAULT_NUMBER_ID) : CONST.DEFAULT_NUMBER_ID; const lastReportActionTransaction = transactions?.[`${ONYXKEYS.COLLECTION.TRANSACTION}${lastReportActionTransactionID}`]; - // SidebarUtils.getOptionData in OptionRowLHNData does not get re-evaluated when the linked task report changes, so we have the lastMessageTextFromReport evaluation logic here - let lastActorDetails: Partial | null = item?.lastActorAccountID && personalDetails?.[item.lastActorAccountID] ? personalDetails[item.lastActorAccountID] : null; - if (!lastActorDetails && lastReportAction) { - const lastActorDisplayName = lastReportAction?.person?.[0]?.text; + const lastActorAccountID = getReportActionActorAccountID(lastAction, undefined, item) ?? item.lastActorAccountID; + let lastActorDetails: Partial | null = lastActorAccountID && personalDetails?.[lastActorAccountID] ? personalDetails[lastActorAccountID] : null; + + if (!lastActorDetails && lastAction) { + const lastActorDisplayName = lastAction?.person?.[0]?.text; lastActorDetails = lastActorDisplayName ? { displayName: lastActorDisplayName, - accountID: item?.lastActorAccountID, + accountID: lastActorAccountID, } : null; } - const movedFromReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastReportAction, CONST.REPORT.MOVE_TYPE.FROM)}`]; - const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastReportAction, CONST.REPORT.MOVE_TYPE.TO)}`]; + + const movedFromReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.FROM)}`]; + const movedToReport = reports?.[`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.TO)}`]; const itemReportMetadata = reportMetadataCollection?.[`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`]; - const lastMessageTextFromReport = getLastMessageTextForReport({ - translate, - report: item, - lastActorDetails, - movedFromReport, - movedToReport, - policy: itemPolicy, - isReportArchived: !!itemReportNameValuePairs?.private_isArchived, - policyForMovingExpensesID, - reportMetadata: itemReportMetadata, - currentUserAccountID, - }); - const shouldShowRBRorGBRTooltip = firstReportIDWithGBRorRBR === reportID; + const shouldAlwaysRecalculateMessage = isReportArchived || isReportPreviewAction(lastAction); + const lastMessageTextFromReport = + (shouldAlwaysRecalculateMessage ? undefined : item.lastMessageText) ?? + getLastMessageTextForReport({ + translate, + report: item, + lastActorDetails, + movedFromReport, + movedToReport, + policy: itemPolicy, + isReportArchived, + policyForMovingExpensesID, + reportMetadata: itemReportMetadata, + visibleReportActionsDataParam: visibleReportActionsData, + lastAction, + currentUserAccountID, + }); - let lastAction: ReportAction | undefined; - if (!itemReportActions || !item) { - lastAction = undefined; - } else { - const canUserPerformWriteAction = canUserPerformWriteActionUtil(item, isReportArchived); - const actionsArray = getSortedReportActions(Object.values(itemReportActions)); - const reportActionsForDisplay = actionsArray.filter( - (reportAction) => shouldReportActionBeVisibleAsLastAction(reportAction, canUserPerformWriteAction) && reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED, - ); - lastAction = reportActionsForDisplay.at(-1); - } + const shouldShowRBRorGBRTooltip = firstReportIDWithGBRorRBR === reportID; let lastActionReport: OnyxEntry | undefined; if (isInviteOrRemovedAction(lastAction)) { @@ -331,6 +330,7 @@ function LHNOptionsList({style, contentContainerStyles, data, onSelectRow, optio isScreenFocused, localeCompare, translate, + visibleReportActionsData, currentUserAccountID, ], ); diff --git a/src/components/LHNOptionsList/OptionRowLHNData.tsx b/src/components/LHNOptionsList/OptionRowLHNData.tsx index b14bf5302973..da78a912afe6 100644 --- a/src/components/LHNOptionsList/OptionRowLHNData.tsx +++ b/src/components/LHNOptionsList/OptionRowLHNData.tsx @@ -51,6 +51,7 @@ function OptionRowLHNData({ const [movedFromReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.FROM)}`, {canBeMissing: true}); const [movedToReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getMovedReportID(lastAction, CONST.REPORT.MOVE_TYPE.TO)}`, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); // Check the report errors equality to avoid re-rendering when there are no changes const prevReportErrors = usePrevious(reportAttributes?.reportErrors); const areReportErrorsEqual = useMemo(() => deepEqual(prevReportErrors, reportAttributes?.reportErrors), [prevReportErrors, reportAttributes?.reportErrors]); @@ -78,6 +79,7 @@ function OptionRowLHNData({ movedFromReport, movedToReport, currentUserAccountID, + visibleReportActionsData, }); if (deepEqual(item, optionItemRef.current)) { return optionItemRef.current; @@ -114,6 +116,7 @@ function OptionRowLHNData({ movedFromReport, movedToReport, currentUserAccountID, + visibleReportActionsData, ]); return ( diff --git a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx index 4e13816372d3..864b9bef3013 100644 --- a/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx +++ b/src/components/MoneyRequestReportView/MoneyRequestReportActionsList.tsx @@ -50,7 +50,7 @@ import { isCurrentActionUnread, isDeletedParentAction, isIOUActionMatchingTransactionList, - shouldReportActionBeVisible, + isReportActionVisible, wasMessageReceivedWhileOffline, } from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction, chatIncludesChronosWithID, getOriginalReportID, getReportLastVisibleActionCreated, isHarvestCreatedExpenseReport, isUnread} from '@libs/ReportUtils'; @@ -171,6 +171,7 @@ function MoneyRequestReportActionsList({ const isReportArchived = useReportIsArchived(reportID); const canPerformWriteAction = canUserPerformWriteAction(report, isReportArchived); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const {shouldUseNarrowLayout} = useResponsiveLayoutOnWideRHP(); @@ -226,17 +227,29 @@ function MoneyRequestReportActionsList({ const visibleReportActions = useMemo(() => { const filteredActions = reportActions.filter((reportAction) => { const isActionVisibleOnMoneyReport = isActionVisibleOnMoneyRequestReport(reportAction, shouldShowHarvestCreatedAction); + if (!isActionVisibleOnMoneyReport) { + return false; + } - return ( - isActionVisibleOnMoneyReport && - (isOffline || isDeletedParentAction(reportAction) || reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE || reportAction.errors) && - shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canPerformWriteAction) && - isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs) - ); + 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; + if (!isReportActionVisible(reportAction, actionReportID, canPerformWriteAction, visibleReportActionsData)) { + return false; + } + + if (!isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)) { + return false; + } + + return true; }); return filteredActions.toReversed(); - }, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, shouldShowHarvestCreatedAction]); + }, [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, shouldShowHarvestCreatedAction, visibleReportActionsData, reportID]); const reportActionSize = useRef(visibleReportActions.length); const lastAction = visibleReportActions.at(-1); diff --git a/src/components/ParentNavigationSubtitle.tsx b/src/components/ParentNavigationSubtitle.tsx index cb1d3ea4939c..e04d280a1628 100644 --- a/src/components/ParentNavigationSubtitle.tsx +++ b/src/components/ParentNavigationSubtitle.tsx @@ -13,7 +13,7 @@ import useThemeStyles from '@hooks/useThemeStyles'; import {isFullScreenName} from '@libs/Navigation/helpers/isNavigatorName'; import Navigation from '@libs/Navigation/Navigation'; import type {RightModalNavigatorParamList} from '@libs/Navigation/types'; -import {getReportAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; +import {getReportAction, isReportActionVisible} from '@libs/ReportActionsUtils'; import {canUserPerformWriteAction as canUserPerformWriteActionReportUtils, isMoneyRequestReport} from '@libs/ReportUtils'; import CONST from '@src/CONST'; import type {ParentNavigationSummaryParams} from '@src/languages/params'; @@ -88,6 +88,7 @@ function ParentNavigationSubtitle({ const {translate} = useLocalize(); const [currentReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, {canBeMissing: false}); const [report] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${parentReportID}`, {canBeMissing: false}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const isReportArchived = useReportIsArchived(report?.reportID); const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report, isReportArchived); const isReportInRHP = currentRoute.name === SCREENS.RIGHT_MODAL.SEARCH_REPORT; @@ -116,7 +117,7 @@ function ParentNavigationSubtitle({ const onPress = () => { const parentAction = getReportAction(parentReportID, parentReportActionID); - const isVisibleAction = shouldReportActionBeVisible(parentAction, parentAction?.reportActionID ?? CONST.DEFAULT_NUMBER_ID, canUserPerformWriteAction); + const isVisibleAction = isReportActionVisible(parentAction, parentReportID, canUserPerformWriteAction, visibleReportActionsData); const focusedNavigatorState = currentFocusedNavigator?.state; const currentReportIndex = focusedNavigatorState?.index; diff --git a/src/components/Search/SearchAutocompleteList.tsx b/src/components/Search/SearchAutocompleteList.tsx index 42a7aee121ce..e839504b4989 100644 --- a/src/components/Search/SearchAutocompleteList.tsx +++ b/src/components/Search/SearchAutocompleteList.tsx @@ -196,6 +196,7 @@ function SearchAutocompleteList({ const [recentSearches] = useOnyx(ONYXKEYS.RECENT_SEARCHES, {canBeMissing: true}); const [countryCode] = useOnyx(ONYXKEYS.COUNTRY_CODE, {canBeMissing: false}); const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const currentUserEmail = currentUserPersonalDetails.email ?? ''; const currentUserAccountID = currentUserPersonalDetails.accountID; @@ -222,10 +223,23 @@ function SearchAutocompleteList({ shouldShowGBR: false, shouldUnreadBeBold: true, loginList, + visibleReportActionsData, currentUserAccountID, currentUserEmail, }); - }, [areOptionsInitialized, options, draftComments, nvpDismissedProductTraining, betas, autocompleteQueryValue, countryCode, loginList, currentUserAccountID, currentUserEmail]); + }, [ + areOptionsInitialized, + options, + draftComments, + nvpDismissedProductTraining, + betas, + autocompleteQueryValue, + countryCode, + loginList, + visibleReportActionsData, + currentUserAccountID, + currentUserEmail, + ]); const [isInitialRender, setIsInitialRender] = useState(true); const parsedQuery = useMemo(() => parseForAutocomplete(autocompleteQueryValue), [autocompleteQueryValue]); @@ -436,6 +450,7 @@ function SearchAutocompleteList({ countryCode, loginList, shouldShowGBR: true, + visibleReportActionsData, currentUserAccountID, currentUserEmail, }).personalDetails.filter((participant) => participant.text && !alreadyAutocompletedKeys.has(participant.text.toLowerCase())); @@ -469,6 +484,7 @@ function SearchAutocompleteList({ countryCode, loginList, shouldShowGBR: true, + visibleReportActionsData, currentUserAccountID, currentUserEmail, }).recentReports.filter((chat) => { @@ -657,6 +673,7 @@ function SearchAutocompleteList({ workspaceList, hasAutocompleteList, isAutocompleteList, + visibleReportActionsData, ]); const sortedRecentSearches = useMemo(() => { diff --git a/src/components/Search/SearchFiltersChatsSelector.tsx b/src/components/Search/SearchFiltersChatsSelector.tsx index 6c221eecde93..c0909488fc95 100644 --- a/src/components/Search/SearchFiltersChatsSelector.tsx +++ b/src/components/Search/SearchFiltersChatsSelector.tsx @@ -64,17 +64,25 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const archivedReportsIdSet = useArchivedReportsIdSet(); const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const selectedOptions = useMemo(() => { return selectedReportIDs.map((id) => { const report = getSelectedOptionData( - createOptionFromReport({...reports?.[`${ONYXKEYS.COLLECTION.REPORT}${id}`], reportID: id}, personalDetails, currentUserAccountID, reportAttributesDerived), + createOptionFromReport( + {...reports?.[`${ONYXKEYS.COLLECTION.REPORT}${id}`], reportID: id}, + personalDetails, + currentUserAccountID, + reportAttributesDerived, + undefined, + visibleReportActionsData, + ), ); const isReportArchived = archivedReportsIdSet.has(`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`); - const alternateText = getAlternateText(report, {}, isReportArchived, currentUserAccountID, {}); + const alternateText = getAlternateText(report, {}, isReportArchived, currentUserAccountID, {}, visibleReportActionsData); return {...report, alternateText}; }); - }, [archivedReportsIdSet, personalDetails, reportAttributesDerived, reports, selectedReportIDs, currentUserAccountID]); + }, [archivedReportsIdSet, personalDetails, reportAttributesDerived, reports, selectedReportIDs, currentUserAccountID, visibleReportActionsData]); const defaultOptions = useMemo(() => { if (!areOptionsInitialized || !isScreenTransitionEnd) { @@ -88,10 +96,22 @@ function SearchFiltersChatsSelector({initialReportIDs, onFiltersUpdate, isScreen isUsedInChatFinder: false, countryCode, loginList, + visibleReportActionsData, currentUserAccountID, currentUserEmail, }); - }, [areOptionsInitialized, isScreenTransitionEnd, options, draftComments, nvpDismissedProductTraining, countryCode, loginList, currentUserAccountID, currentUserEmail]); + }, [ + areOptionsInitialized, + isScreenTransitionEnd, + options, + draftComments, + nvpDismissedProductTraining, + countryCode, + loginList, + visibleReportActionsData, + currentUserAccountID, + currentUserEmail, + ]); const chatOptions = useMemo(() => { return filterAndOrderOptions(defaultOptions, cleanSearchTerm, countryCode, loginList, currentUserEmail, currentUserAccountID, { diff --git a/src/hooks/usePaginatedReportActions.ts b/src/hooks/usePaginatedReportActions.ts index c49dc1af84fa..fb3f9b7834c0 100644 --- a/src/hooks/usePaginatedReportActions.ts +++ b/src/hooks/usePaginatedReportActions.ts @@ -27,9 +27,9 @@ function usePaginatedReportActions(reportID: string | undefined, reportActionID? const getSortedAllReportActionsSelector = useCallback( (allReportActions: OnyxEntry): ReportAction[] => { - return getSortedReportActionsForDisplay(allReportActions, hasWriteAccess, true); + return getSortedReportActionsForDisplay(allReportActions, hasWriteAccess, true, undefined, nonEmptyStringReportID); }, - [hasWriteAccess], + [hasWriteAccess, nonEmptyStringReportID], ); const [sortedAllReportActions] = useOnyx( diff --git a/src/hooks/useSearchSelector.base.ts b/src/hooks/useSearchSelector.base.ts index 27d7e2317d96..5cc956993bd8 100644 --- a/src/hooks/useSearchSelector.base.ts +++ b/src/hooks/useSearchSelector.base.ts @@ -167,6 +167,7 @@ function useSearchSelectorBase({ const [allPolicies] = useOnyx(ONYXKEYS.COLLECTION.POLICY, {canBeMissing: true}); const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const currentUserAccountID = currentUserPersonalDetails.accountID; const currentUserEmail = currentUserPersonalDetails.email ?? ''; @@ -202,6 +203,7 @@ function useSearchSelectorBase({ includeUserToInvite, countryCode, loginList, + visibleReportActionsData, currentUserAccountID, currentUserEmail, }); @@ -297,6 +299,7 @@ function useSearchSelectorBase({ areOptionsInitialized, searchContext, optionsWithContacts, + allPolicies, draftComments, nvpDismissedProductTraining, betas, @@ -311,6 +314,7 @@ function useSearchSelectorBase({ getValidOptionsConfig, selectedOptions, includeCurrentUser, + visibleReportActionsData, currentUserAccountID, currentUserEmail, personalDetails, diff --git a/src/libs/OptionsListUtils/index.ts b/src/libs/OptionsListUtils/index.ts index be94b734fc2b..4f9c2bd85633 100644 --- a/src/libs/OptionsListUtils/index.ts +++ b/src/libs/OptionsListUtils/index.ts @@ -45,6 +45,7 @@ import { getInvoiceCompanyWebsiteUpdateMessage, getIOUReportIDFromReportActionPreview, getJoinRequestMessage, + getLastVisibleAction, getLastVisibleMessage, getMarkedReimbursedMessage, getMentionedAccountIDsFromAction, @@ -86,12 +87,11 @@ import { isReimbursementDeQueuedOrCanceledAction, isReimbursementQueuedAction, isRenamedAction, + isReportActionVisible, isReportPreviewAction, isTaskAction, isThreadParentMessage, isUnapprovedAction, - isWhisperAction, - shouldReportActionBeVisible, withDEWRoutedActionsArray, } from '@libs/ReportActionsUtils'; import {computeReportName} from '@libs/ReportNameUtils'; @@ -166,6 +166,7 @@ import type { ReportAttributesDerivedValue, ReportMetadata, ReportNameValuePairs, + VisibleReportActionsDerivedValue, } from '@src/types/onyx'; import type {Attendee, Participant} from '@src/types/onyx/IOU'; import {isEmptyObject} from '@src/types/utils/EmptyObject'; @@ -241,7 +242,6 @@ Onyx.connect({ const lastReportActions: ReportActions = {}; const allSortedReportActions: Record = {}; let allReportActions: OnyxCollection; -const lastVisibleReportActions: ReportActions = {}; Onyx.connect({ key: ONYXKEYS.COLLECTION.REPORT_ACTIONS, waitForCollectionCallback: true, @@ -252,7 +252,7 @@ Onyx.connect({ allReportActions = actions ?? {}; - // Iterate over the report actions to build the sorted and lastVisible report actions objects + // Iterate over the report actions to build the sorted report actions objects for (const reportActions of Object.entries(allReportActions)) { const reportID = reportActions[0].split('_').at(1); if (!reportID) { @@ -271,6 +271,7 @@ Onyx.connect({ if (transactionThreadReportID) { const transactionThreadReportActionsArray = Object.values(actions[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${transactionThreadReportID}`] ?? {}); sortedReportActions = getCombinedReportActions(sortedReportActions, transactionThreadReportID, transactionThreadReportActionsArray, reportID); + allSortedReportActions[reportID] = sortedReportActions; } const firstReportAction = sortedReportActions.at(0); @@ -279,26 +280,6 @@ Onyx.connect({ } else { lastReportActions[reportID] = firstReportAction; } - - const reportNameValuePairs = allReportNameValuePairsOnyxConnect?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; - const isReportArchived = !!reportNameValuePairs?.private_isArchived; - const isWriteActionAllowed = canUserPerformWriteAction(report, isReportArchived); - - // The report is only visible if it is the last action not deleted that - // does not match a closed or created state. - const reportActionsForDisplay = sortedReportActions.filter( - (reportAction, actionKey) => - (!(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) || isActionableMentionWhisper(reportAction)) && - shouldReportActionBeVisible(reportAction, actionKey, isWriteActionAllowed) && - reportAction.actionName !== CONST.REPORT.ACTIONS.TYPE.CREATED && - reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE, - ); - const reportActionForDisplay = reportActionsForDisplay.at(0); - if (!reportActionForDisplay) { - delete lastVisibleReportActions[reportID]; - continue; - } - lastVisibleReportActions[reportID] = reportActionForDisplay; } }, }); @@ -426,8 +407,8 @@ function shouldShowLastActorDisplayName( lastAction: OnyxEntry, currentUserAccountIDParam: number, ) { - const reportID = report?.reportID; - const lastReportAction = (reportID ? lastVisibleReportActions[reportID] : undefined) ?? lastAction; + // Use lastAction directly instead of getLastVisibleReportAction to avoid using stale cache data + const lastReportAction = lastAction; // Use report.lastActionType as fallback when report actions aren't loaded yet (e.g., on cold start) const lastActionName = lastReportAction?.actionName ?? report?.lastActionType; @@ -462,27 +443,35 @@ function getAlternateText( isReportArchived: boolean | undefined, currentUserAccountID: number, lastActorDetails: Partial | null = {}, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, + translate?: LocalizedTranslate, ) { const report = getReportOrDraftReport(option.reportID); const isAdminRoom = reportUtilsIsAdminRoom(report); const isAnnounceRoom = reportUtilsIsAnnounceRoom(report); const isGroupChat = reportUtilsIsGroupChat(report); const isExpenseThread = isMoneyRequest(report); + // eslint-disable-next-line @typescript-eslint/no-deprecated + const translateFn = translate ?? translateLocal; const formattedLastMessageText = formatReportLastMessageText(Parser.htmlToText(option.lastMessageText ?? '')) || - // eslint-disable-next-line @typescript-eslint/no-deprecated - getLastMessageTextForReport({translate: translateLocal, report, lastActorDetails, isReportArchived, currentUserAccountID}); + getLastMessageTextForReport({ + translate: translateFn, + report, + lastActorDetails, + isReportArchived, + visibleReportActionsDataParam: visibleReportActionsData, + currentUserAccountID, + }); const reportPrefix = getReportSubtitlePrefix(report); const formattedLastMessageTextWithPrefix = reportPrefix + formattedLastMessageText; if (isExpenseThread || option.isMoneyRequestReport) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateLocal('iou.expense'); + return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateFn('iou.expense'); } if (option.isThread) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateLocal('threads.thread'); + return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateFn('threads.thread'); } if (option.isChatRoom && !isAdminRoom && !isAnnounceRoom) { @@ -494,13 +483,11 @@ function getAlternateText( } if (option.isTaskReport) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateLocal('task.task'); + return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateFn('task.task'); } if (isGroupChat) { - // eslint-disable-next-line @typescript-eslint/no-deprecated - return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateLocal('common.group'); + return showChatPreviewLine && formattedLastMessageText ? formattedLastMessageTextWithPrefix : translateFn('common.group'); } return showChatPreviewLine && formattedLastMessageText @@ -549,15 +536,19 @@ function isSearchStringMatchUserDetails(personalDetail: PersonalDetails, searchV /** * Get IOU report ID of report last action if the action is report action preview */ -function getIOUReportIDOfLastAction(report: OnyxEntry): string | undefined { +function getIOUReportIDOfLastAction(report: OnyxEntry, visibleReportActionsData?: VisibleReportActionsDerivedValue, lastAction?: OnyxEntry): string | undefined { if (!report?.reportID) { return; } - const lastAction = lastVisibleReportActions[report.reportID]; - if (!isReportPreviewAction(lastAction)) { + // Use lastAction if available (from useOnyx), otherwise fallback to getLastVisibleAction which uses isReportActionVisibleAsLastAction with proper filters + const reportNameValuePairs = allReportNameValuePairsOnyxConnect?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${report.reportID}`]; + const isReportArchived = !!reportNameValuePairs?.private_isArchived; + const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); + const action = lastAction ?? getLastVisibleAction(report.reportID, canUserPerformWrite, {}, undefined, visibleReportActionsData); + if (!isReportPreviewAction(action)) { return; } - return getReportOrDraftReport(getIOUReportIDFromReportActionPreview(lastAction))?.reportID; + return getReportOrDraftReport(getIOUReportIDFromReportActionPreview(action))?.reportID; } function hasHiddenDisplayNames(accountIDs: number[]) { @@ -568,15 +559,21 @@ function getLastActorDisplayNameFromLastVisibleActions( report: OnyxEntry, lastActorDetails: Partial | null, currentUserAccountIDParam: number, - personalDetails: OnyxEntry, + personalDetails?: OnyxEntry, + visibleReportActionsData?: VisibleReportActionsDerivedValue, + lastAction?: OnyxEntry, ): string { const reportID = report?.reportID; - const lastReportAction = reportID ? lastVisibleReportActions[reportID] : undefined; + // Use lastAction if available (from useOnyx), otherwise fallback to getLastVisibleAction which uses isReportActionVisibleAsLastAction with proper filters + const reportNameValuePairs = reportID ? allReportNameValuePairsOnyxConnect?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`] : undefined; + const isReportArchived = !!reportNameValuePairs?.private_isArchived; + const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); + const lastReportAction = lastAction ?? getLastVisibleAction(reportID, canUserPerformWrite, {}, undefined, visibleReportActionsData); if (lastReportAction) { // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing const lastActorAccountID = getReportActionActorAccountID(lastReportAction, undefined, undefined) || report?.lastActorAccountID; - let actorDetails: Partial | null = lastActorAccountID ? (personalDetails?.[lastActorAccountID] ?? null) : null; + let actorDetails: Partial | null = lastActorAccountID ? ((personalDetails ?? allPersonalDetails)?.[lastActorAccountID] ?? null) : null; if (!actorDetails && lastReportAction.person?.at(0)?.text) { actorDetails = { @@ -606,6 +603,8 @@ function getLastMessageTextForReport({ isReportArchived = false, policyForMovingExpensesID, reportMetadata, + visibleReportActionsDataParam, + lastAction, currentUserAccountID, }: { translate: LocalizedTranslate; @@ -617,11 +616,15 @@ function getLastMessageTextForReport({ isReportArchived?: boolean; policyForMovingExpensesID?: string; reportMetadata?: OnyxEntry; - currentUserAccountID: number; + visibleReportActionsDataParam?: VisibleReportActionsDerivedValue; + lastAction?: OnyxEntry; + currentUserAccountID?: number; }): string { const reportID = report?.reportID; - const lastReportAction = reportID ? lastVisibleReportActions[reportID] : undefined; - const lastVisibleMessage = getLastVisibleMessage(report?.reportID); + // Use lastAction if available (from useOnyx), otherwise fallback to getLastVisibleAction which uses isReportActionVisibleAsLastAction with proper filters + const canUserPerformWrite = canUserPerformWriteAction(report, isReportArchived); + const lastReportAction = lastAction ?? getLastVisibleAction(reportID, canUserPerformWrite, {}, undefined, visibleReportActionsDataParam); + const lastVisibleMessage = getLastVisibleMessage(report?.reportID, undefined, {}, undefined, visibleReportActionsDataParam); // some types of actions are filtered out for lastReportAction, in some cases we need to check the actual last action const lastOriginalReportAction = reportID ? lastReportActions[reportID] : undefined; @@ -654,21 +657,29 @@ function getLastMessageTextForReport({ lastMessageTextFromReport = formatReportLastMessageText(Parser.htmlToText(properSchemaForMoneyRequestMessage)); } else if (isReportPreviewAction(lastReportAction)) { const iouReport = getReportOrDraftReport(getIOUReportIDFromReportActionPreview(lastReportAction)); - const lastIOUMoneyReportAction = iouReport?.reportID - ? allSortedReportActions[iouReport.reportID]?.find( - (reportAction, key): reportAction is ReportAction => - shouldReportActionBeVisible(reportAction, key, canUserPerformWriteAction(report, isReportArchived)) && - reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && - isMoneyRequestAction(reportAction), - ) - : undefined; - // For workspace chats, use the report title - if (reportUtilsIsPolicyExpenseChat(report) && !isEmptyObject(iouReport)) { + const iouReportID = iouReport?.reportID; + const reportCache = iouReportID ? visibleReportActionsDataParam?.[iouReportID] : undefined; + const visibleReportActionsForIOUReport = reportCache && Object.keys(reportCache).length > 0 ? visibleReportActionsDataParam : undefined; + const iouReportActions = iouReportID ? allSortedReportActions[iouReportID] : undefined; + const lastIOUMoneyReportAction = + iouReportID && iouReportActions + ? iouReportActions.find( + (reportAction): reportAction is ReportAction => + isReportActionVisible(reportAction, iouReportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsForIOUReport) && + reportAction.pendingAction !== CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE && + isMoneyRequestAction(reportAction), + ) + : undefined; + + if (reportUtilsIsPolicyExpenseChat(report) && iouReport?.reportID) { + const reportName = computeReportName(iouReport, undefined, undefined, undefined, undefined, undefined, undefined, currentUserAccountID); + lastMessageTextFromReport = formatReportLastMessageText(reportName); + } else if (!lastIOUMoneyReportAction && iouReport?.reportID) { const reportName = computeReportName(iouReport, undefined, undefined, undefined, undefined, undefined, undefined, currentUserAccountID); lastMessageTextFromReport = formatReportLastMessageText(reportName); } else { const reportPreviewMessage = getReportPreviewMessage( - !isEmptyObject(iouReport) ? iouReport : null, + iouReport?.reportID ? iouReport : null, lastIOUMoneyReportAction ?? lastReportAction, true, reportUtilsIsChatReport(report), @@ -866,6 +877,8 @@ function createOption( config?: PreviewConfig, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], privateIsArchived?: string, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, + translate?: LocalizedTranslate, ): SearchOptionData { const {showChatPreviewLine = false, forcePolicyNamePreview = false, showPersonalDetails = false, selected, isSelected, isDisabled} = config ?? {}; @@ -942,11 +955,27 @@ function createOption( // If displaying chat preview line is needed, let's overwrite the default alternate text const lastActorDetails = personalDetails?.[report?.lastActorAccountID ?? String(CONST.DEFAULT_NUMBER_ID)] ?? {}; // eslint-disable-next-line @typescript-eslint/no-deprecated - result.lastMessageText = getLastMessageTextForReport({translate: translateLocal, report, lastActorDetails, isReportArchived: !!result.private_isArchived, currentUserAccountID}); + const translateFn = translate ?? translateLocal; + result.lastMessageText = getLastMessageTextForReport({ + translate: translateFn, + report, + lastActorDetails, + isReportArchived: !!result.private_isArchived, + visibleReportActionsDataParam: visibleReportActionsData, + currentUserAccountID, + }); result.alternateText = showPersonalDetails && personalDetail?.login ? personalDetail.login - : getAlternateText(result, {showChatPreviewLine, forcePolicyNamePreview}, !!result.private_isArchived, currentUserAccountID, lastActorDetails); + : getAlternateText( + result, + {showChatPreviewLine, forcePolicyNamePreview}, + !!result.private_isArchived, + currentUserAccountID, + lastActorDetails, + visibleReportActionsData, + translateFn, + ); const personalDetailsForCompute: PersonalDetailsList | undefined = personalDetails ?? undefined; const computedReportName = computeReportName( @@ -1009,6 +1038,7 @@ function getReportOption( personalDetails: OnyxEntry, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], reportDrafts?: OnyxCollection, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): OptionData { const report = getReportOrDraftReport(participant.reportID, undefined, undefined, reportDrafts); const visibleParticipantAccountIDs = getParticipantsAccountIDsForDisplay(report, true); @@ -1024,6 +1054,7 @@ function getReportOption( }, reportAttributesDerived, privateIsArchived, + visibleReportActionsData, ); // Update text & alternateText because createOption returns workspace name only if report is owned by the user @@ -1067,6 +1098,7 @@ function getReportDisplayOption( personalDetails: OnyxEntry, privateIsArchived: string | undefined, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): OptionData { const visibleParticipantAccountIDs = getParticipantsAccountIDsForDisplay(report, true); @@ -1081,6 +1113,7 @@ function getReportDisplayOption( }, reportAttributesDerived, privateIsArchived, + visibleReportActionsData, ); // Update text & alternateText because createOption returns workspace name only if report is owned by the user @@ -1114,6 +1147,7 @@ function getPolicyExpenseReportOption( currentUserAccountID: number, personalDetails: OnyxEntry, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): SearchOptionData { const expenseReport = reportUtilsIsPolicyExpenseChat(participant) ? getReportOrDraftReport(participant.reportID) : null; @@ -1131,6 +1165,8 @@ function getPolicyExpenseReportOption( forcePolicyNamePreview: false, }, reportAttributesDerived, + undefined, + visibleReportActionsData, ); // Update text & alternateText because createOption returns workspace name only if report is owned by the user @@ -1241,6 +1277,7 @@ function processReport( personalDetails: OnyxEntry, currentUserAccountID: number, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): { reportMapEntry?: [number, Report]; // The entry to add to reportMapForAccountIDs if applicable reportOption: SearchOption | null; // The report option to add to allReportOptions if applicable @@ -1264,7 +1301,7 @@ function processReport( reportMapEntry, reportOption: { item: report, - ...createOption(accountIDs, personalDetails, report, currentUserAccountID, undefined, reportAttributesDerived), + ...createOption(accountIDs, personalDetails, report, currentUserAccountID, undefined, reportAttributesDerived, undefined, visibleReportActionsData), }, }; } @@ -1274,6 +1311,7 @@ function createOptionList( currentUserAccountID: number, reports?: OnyxCollection, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ) { const span = Sentry.startInactiveSpan({name: 'createOptionList'}); @@ -1282,7 +1320,7 @@ function createOptionList( if (reports) { for (const report of Object.values(reports)) { - const {reportMapEntry, reportOption} = processReport(report, personalDetails, currentUserAccountID, reportAttributesDerived); + const {reportMapEntry, reportOption} = processReport(report, personalDetails, currentUserAccountID, reportAttributesDerived, visibleReportActionsData); if (reportMapEntry) { const [accountID, reportValue] = reportMapEntry; @@ -1306,6 +1344,8 @@ function createOptionList( showPersonalDetails: true, }, reportAttributesDerived, + undefined, + visibleReportActionsData, ), })); @@ -1345,6 +1385,7 @@ function createFilteredOptionList( searchTerm?: string; betas?: OnyxEntry; } = {}, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ) { const {maxRecentReports = 500, includeP2P = true, searchTerm = ''} = options; const reportMapForAccountIDs: Record = {}; @@ -1394,7 +1435,7 @@ function createFilteredOptionList( // Step 5: Process the limited set of reports (performance optimization) const reportOptions: Array> = []; for (const report of limitedReports) { - const {reportMapEntry, reportOption} = processReport(report, personalDetails, currentUserAccountID, reportAttributesDerived); + const {reportMapEntry, reportOption} = processReport(report, personalDetails, currentUserAccountID, reportAttributesDerived, visibleReportActionsData); if (reportMapEntry) { const [accountID, reportValue] = reportMapEntry; @@ -1424,7 +1465,16 @@ function createFilteredOptionList( return { item: personalDetail, - ...createOption([accountID], personalDetails, reportMapForAccountIDs[accountID], currentUserAccountID, {showPersonalDetails: true}, reportAttributesDerived), + ...createOption( + [accountID], + personalDetails, + reportMapForAccountIDs[accountID], + currentUserAccountID, + {showPersonalDetails: true}, + reportAttributesDerived, + undefined, + visibleReportActionsData, + ), }; }) : []; @@ -1441,12 +1491,13 @@ function createOptionFromReport( currentUserAccountID: number, reportAttributesDerived?: ReportAttributesDerivedValue['reports'], config?: PreviewConfig, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ) { const accountIDs = getParticipantsAccountIDsForDisplay(report); return { item: report, - ...createOption(accountIDs, personalDetails, report, currentUserAccountID, config, reportAttributesDerived), + ...createOption(accountIDs, personalDetails, report, currentUserAccountID, config, reportAttributesDerived, undefined, visibleReportActionsData), }; } @@ -1733,7 +1784,8 @@ function getUserToInviteOption({ loginList = {}, currentUserEmail, currentUserAccountID, -}: GetUserToInviteConfig): SearchOptionData | null { + visibleReportActionsData = {}, +}: GetUserToInviteConfig & {visibleReportActionsData?: VisibleReportActionsDerivedValue}): SearchOptionData | null { if (!searchValue) { return null; } @@ -1761,9 +1813,18 @@ function getUserToInviteOption({ login: searchValue, }, }; - const userToInvite = createOption([optimisticAccountID], personalDetailsExtended, null, currentUserAccountID, { - showChatPreviewLine, - }); + const userToInvite = createOption( + [optimisticAccountID], + personalDetailsExtended, + null, + currentUserAccountID, + { + showChatPreviewLine, + }, + undefined, + undefined, + visibleReportActionsData, + ); userToInvite.isOptimisticAccount = true; userToInvite.login = searchValue; // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing @@ -2033,6 +2094,7 @@ function prepareReportOptionsForDisplay( policiesCollection: OnyxCollection, currentUserAccountID: number, config: GetValidReportsConfig, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): Array> { const { showChatPreviewLine = false, @@ -2063,7 +2125,7 @@ function prepareReportOptionsForDisplay( * By default, generated options does not have the chat preview line enabled. * If showChatPreviewLine or forcePolicyNamePreview are true, let's generate and overwrite the alternate text. */ - const alternateText = getAlternateText(option, {showChatPreviewLine, forcePolicyNamePreview}, !!option.private_isArchived, currentUserAccountID); + const alternateText = getAlternateText(option, {showChatPreviewLine, forcePolicyNamePreview}, !!option.private_isArchived, currentUserAccountID, null, visibleReportActionsData); const isSelected = isReportSelected(option, selectedOptions); let isOptionUnread = option.isUnread; @@ -2201,6 +2263,7 @@ function getValidOptions( ...config }: GetOptionsConfig = {}, countryCode: number = CONST.DEFAULT_COUNTRY_CODE, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): Options { const restrictedLogins = getRestrictedLogins(config, options, canShowManagerMcTest, nvpDismissedProductTraining); @@ -2274,7 +2337,31 @@ function getValidOptions( [selfDMChats, workspaceChats, recentReportOptions] = optionsOrderAndGroupBy([isSelfDMChat, isWorkspaceChat], options.reports, recentReportComparator, maxElements, filteringFunction); if (selfDMChats.length > 0) { - selfDMChat = prepareReportOptionsForDisplay(selfDMChats, policiesCollection, currentUserAccountID, { + selfDMChat = prepareReportOptionsForDisplay( + selfDMChats, + policiesCollection, + currentUserAccountID, + { + ...getValidReportsConfig, + selectedOptions, + shouldBoldTitleByDefault, + shouldSeparateSelfDMChat, + shouldSeparateWorkspaceChat, + shouldShowGBR, + personalDetails, + }, + visibleReportActionsData, + ).at(0); + } + + if (maxRecentReportElements) { + recentReportOptions = recentReportOptions.splice(0, maxRecentReportElements); + } + recentReportOptions = prepareReportOptionsForDisplay( + recentReportOptions, + policiesCollection, + currentUserAccountID, + { ...getValidReportsConfig, selectedOptions, shouldBoldTitleByDefault, @@ -2282,31 +2369,25 @@ function getValidOptions( shouldSeparateWorkspaceChat, shouldShowGBR, personalDetails, - }).at(0); - } - - if (maxRecentReportElements) { - recentReportOptions = recentReportOptions.splice(0, maxRecentReportElements); - } - recentReportOptions = prepareReportOptionsForDisplay(recentReportOptions, policiesCollection, currentUserAccountID, { - ...getValidReportsConfig, - selectedOptions, - shouldBoldTitleByDefault, - shouldSeparateSelfDMChat, - shouldSeparateWorkspaceChat, - shouldShowGBR, - personalDetails, - }); + }, + visibleReportActionsData, + ); - workspaceChats = prepareReportOptionsForDisplay(workspaceChats, policiesCollection, currentUserAccountID, { - ...getValidReportsConfig, - selectedOptions, - shouldBoldTitleByDefault, - shouldSeparateSelfDMChat, - shouldSeparateWorkspaceChat, - shouldShowGBR, - personalDetails, - }); + workspaceChats = prepareReportOptionsForDisplay( + workspaceChats, + policiesCollection, + currentUserAccountID, + { + ...getValidReportsConfig, + selectedOptions, + shouldBoldTitleByDefault, + shouldSeparateSelfDMChat, + shouldSeparateWorkspaceChat, + shouldShowGBR, + personalDetails, + }, + visibleReportActionsData, + ); } else if (recentAttendees && recentAttendees?.length > 0) { recentAttendees.filter((attendee) => { const login = attendee.login ?? attendee.displayName; @@ -2423,6 +2504,7 @@ type SearchOptionsConfig = { shouldShowGBR?: boolean; shouldUnreadBeBold?: boolean; loginList: OnyxEntry; + visibleReportActionsData?: VisibleReportActionsDerivedValue; currentUserAccountID: number; currentUserEmail: string; personalDetails?: OnyxEntry; @@ -2447,6 +2529,7 @@ function getSearchOptions({ shouldShowGBR = false, shouldUnreadBeBold = false, loginList, + visibleReportActionsData = {}, currentUserAccountID, currentUserEmail, }: SearchOptionsConfig): Options { @@ -2484,6 +2567,7 @@ function getSearchOptions({ shouldUnreadBeBold, }, countryCode, + visibleReportActionsData, ); Timing.end(CONST.TIMING.LOAD_SEARCH_OPTIONS); @@ -2589,6 +2673,7 @@ function getMemberInviteOptions( excludeLogins: Record = {}, includeSelectedOptions = false, countryCode: number = CONST.DEFAULT_COUNTRY_CODE, + visibleReportActionsData: VisibleReportActionsDerivedValue = {}, ): Options { return getValidOptions( {personalDetails, reports: []}, @@ -2608,6 +2693,7 @@ function getMemberInviteOptions( maxElements: undefined, }, countryCode, + visibleReportActionsData, ); } diff --git a/src/libs/ReportActionsUtils.ts b/src/libs/ReportActionsUtils.ts index 33c10db00101..fcd2631b1c87 100644 --- a/src/libs/ReportActionsUtils.ts +++ b/src/libs/ReportActionsUtils.ts @@ -15,7 +15,17 @@ import IntlStore from '@src/languages/IntlStore'; import type {TranslationPaths} from '@src/languages/types'; import ONYXKEYS from '@src/ONYXKEYS'; import ROUTES from '@src/ROUTES'; -import type {Card, OnyxInputOrEntry, OriginalMessageIOU, PersonalDetails, Policy, PrivatePersonalDetails, ReportMetadata, ReportNameValuePairs} from '@src/types/onyx'; +import type { + Card, + OnyxInputOrEntry, + OriginalMessageIOU, + PersonalDetails, + Policy, + PrivatePersonalDetails, + ReportMetadata, + ReportNameValuePairs, + VisibleReportActionsDerivedValue, +} from '@src/types/onyx'; import type { JoinWorkspaceResolution, OriginalMessageChangeLog, @@ -224,7 +234,7 @@ function getHtmlWithAttachmentID(html: string, reportActionID: string | undefine } function getReportActionMessage(reportAction: PartialReportAction) { - return Array.isArray(reportAction?.message) ? reportAction.message.at(0) : reportAction?.message; + return Array.isArray(reportAction?.message) ? reportAction?.message.at(0) : reportAction?.message; } function isDeletedParentAction(reportAction: OnyxInputOrEntry): boolean { @@ -409,7 +419,7 @@ function getOriginalMessage(reportAction: OnyxInputO return reportAction?.message ?? reportAction?.originalMessage; } // eslint-disable-next-line @typescript-eslint/no-deprecated - return reportAction.originalMessage; + return reportAction?.originalMessage; } function getMarkedReimbursedMessage(reportAction: OnyxInputOrEntry): string { @@ -1168,6 +1178,75 @@ function shouldReportActionBeVisibleAsLastAction(reportAction: OnyxInputOrEntry< ); } +/** + * Checks if a report action is visible using the pre-computed derived value when available, + * falling back to runtime calculation if not. + */ +function isReportActionVisible( + reportAction: OnyxEntry, + reportID: string, + canUserPerformWriteAction?: boolean, + visibleReportActions?: VisibleReportActionsDerivedValue, +): boolean { + if (!reportAction?.reportActionID) { + return false; + } + + if (reportAction.pendingAction) { + return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction); + } + + if (visibleReportActions) { + const reportCache = visibleReportActions[reportID]; + if (!reportCache) { + return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction); + } + const staticVisibility = reportCache[reportAction.reportActionID]; + // If action is not in derived value cache, fall back to runtime calculation + if (staticVisibility === undefined) { + return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction); + } + if (!staticVisibility) { + return false; + } + if (!canUserPerformWriteAction && isActionableWhisperRequiringWritePermission(reportAction)) { + return false; + } + return true; + } + return shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction); +} + +/** + * Checks if a report action is visible as last action using the pre-computed derived value when available, + * falling back to runtime calculation if not. + */ +function isReportActionVisibleAsLastAction( + reportAction: OnyxInputOrEntry, + canUserPerformWriteAction?: boolean, + visibleReportActions?: VisibleReportActionsDerivedValue, + reportID?: string, +): boolean { + if (!reportAction) { + return false; + } + + if (Object.keys(reportAction.errors ?? {}).length > 0) { + return false; + } + + const actionReportID = reportAction.reportID ?? reportID; + if (!actionReportID) { + return false; + } + + return ( + isReportActionVisible(reportAction, actionReportID, canUserPerformWriteAction, visibleReportActions) && + (!(isWhisperAction(reportAction) && !isReportPreviewAction(reportAction) && !isMoneyRequestAction(reportAction)) || isActionableMentionWhisper(reportAction)) && + !(isDeletedAction(reportAction) && !isDeletedParentAction(reportAction) && !isPendingHide(reportAction)) + ); +} + /** * For policy change logs, report URLs are generated in the server, * which includes a baseURL placeholder that's replaced in the client. @@ -1199,6 +1278,7 @@ function getLastVisibleAction( canUserPerformWriteAction?: boolean, actionsToMerge: Record | null> = {}, reportActionsParam: OnyxCollection = allReportActions, + visibleReportActionsData?: VisibleReportActionsDerivedValue, ): OnyxEntry { let reportActions: Array = []; if (!isEmpty(actionsToMerge)) { @@ -1206,9 +1286,13 @@ function getLastVisibleAction( ReportAction | null | undefined >; } else { - reportActions = Object.values(allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}); + reportActions = Object.values(reportActionsParam?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`] ?? {}); } - const visibleReportActions = reportActions.filter((action): action is ReportAction => shouldReportActionBeVisibleAsLastAction(action, canUserPerformWriteAction)); + // Pass reportID as fallback for actions that don't have it set (e.g., optimistic actions) + // This is cleaner than mutating the actions array + const visibleReportActions = reportActions.filter((action): action is ReportAction => + isReportActionVisibleAsLastAction(action, canUserPerformWriteAction, visibleReportActionsData, reportID), + ); const sortedReportActions = getSortedReportActions(visibleReportActions, true); if (sortedReportActions.length === 0) { return undefined; @@ -1236,8 +1320,9 @@ function getLastVisibleMessage( canUserPerformWriteAction?: boolean, actionsToMerge: Record | null> = {}, reportAction: OnyxInputOrEntry | undefined = undefined, + visibleReportActionsData?: VisibleReportActionsDerivedValue, ): LastVisibleMessage { - const lastVisibleAction = reportAction ?? getLastVisibleAction(reportID, canUserPerformWriteAction, actionsToMerge); + const lastVisibleAction = reportAction ?? getLastVisibleAction(reportID, canUserPerformWriteAction, actionsToMerge, undefined, visibleReportActionsData); const message = getReportActionMessage(lastVisibleAction); if (message && isReportMessageAttachment(message)) { @@ -1339,6 +1424,8 @@ function getSortedReportActionsForDisplay( reportActions: OnyxEntry | ReportAction[], canUserPerformWriteAction?: boolean, shouldIncludeInvisibleActions = false, + visibleReportActionsData?: VisibleReportActionsDerivedValue, + reportID?: string, ): ReportAction[] { let filteredReportActions: ReportAction[] = []; if (!reportActions) { @@ -1349,7 +1436,16 @@ function getSortedReportActionsForDisplay( filteredReportActions = Object.values(reportActions).filter(Boolean); } else { filteredReportActions = Object.entries(reportActions) - .filter(([key, reportAction]) => shouldReportActionBeVisible(reportAction, key, canUserPerformWriteAction)) + .filter(([collectionKey, reportAction]) => { + if (isReportActionDeprecated(reportAction, collectionKey)) { + return false; + } + const actionReportID = reportAction?.reportID ?? reportID; + if (!actionReportID) { + return false; + } + return isReportActionVisible(reportAction, actionReportID, canUserPerformWriteAction, visibleReportActionsData); + }) .map(([, reportAction]) => reportAction); } @@ -1648,9 +1744,16 @@ function getOneTransactionThreadReportID(...args: Parameters shouldReportActionBeVisibleAsLastAction(action, canUserPerformWriteAction)); + const visibleReportActions = Object.values(reportActions ?? {}).filter((action) => + isReportActionVisibleAsLastAction(action, canUserPerformWriteAction, visibleReportActionsData, reportID), + ); // Exclude the task system message and the created message const visibleReportActionsWithoutTaskSystemMessage = visibleReportActions.filter((action) => !isTaskAction(action) && !isCreatedAction(action)); @@ -1814,7 +1917,7 @@ function getMessageOfOldDotLegacyAction(legacyAction: PartialReportAction) { if (!Array.isArray(legacyAction?.message)) { return getReportActionText(legacyAction); } - if (legacyAction.message.length !== 0) { + if (legacyAction?.message.length !== 0) { // Sometime html can be an empty string // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing return legacyAction?.message?.map((element) => getTextFromHtml(element?.html || element?.text)).join('') ?? ''; @@ -2304,6 +2407,22 @@ function isActionableCardFraudAlert(reportAction: OnyxInputOrEntry return reportAction?.actionName === CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_CARD_FRAUD_ALERT; } +/** + * Checks if a report action is an actionable whisper that requires write permission to be visible. + */ +function isActionableWhisperRequiringWritePermission(reportAction: OnyxEntry): boolean { + if (!reportAction) { + return false; + } + + return ( + isActionableReportMentionWhisper(reportAction) || + isActionableJoinRequestPendingReportAction(reportAction) || + isActionableMentionWhisper(reportAction) || + isActionableCardFraudAlert(reportAction) + ); +} + function getExportIntegrationLastMessageText(translate: LocalizedTranslate, reportAction: OnyxEntry): string { const fragments = getExportIntegrationActionFragments(translate, reportAction); return fragments.reduce((acc, fragment) => `${acc} ${fragment.text}`, ''); @@ -3857,6 +3976,7 @@ export { isActionableWhisper, isActionableJoinRequest, isActionableJoinRequestPending, + isActionableJoinRequestPendingReportAction, isActionableMentionWhisper, isActionableMentionInviteToSubmitExpenseConfirmWhisper, isActionableReportMentionWhisper, @@ -3911,6 +4031,7 @@ export { isTrackExpenseAction, isTransactionThread, isTripPreview, + isTravelUpdate, isHoldAction, isWhisperAction, isSubmittedAction, @@ -3931,14 +4052,18 @@ export { isTagModificationAction, isIOUActionMatchingTransactionList, isResolvedActionableWhisper, + isVisiblePreviewOrMoneyRequest, isReimbursementDirectionInformationRequiredAction, shouldHideNewMarker, shouldReportActionBeVisible, shouldReportActionBeVisibleAsLastAction, + isReportActionVisible, + isReportActionVisibleAsLastAction, wasActionTakenByCurrentUser, isInviteOrRemovedAction, isActionableAddPaymentCard, isActionableCardFraudAlert, + isActionableWhisperRequiringWritePermission, getExportIntegrationActionFragments, getExportIntegrationLastMessageText, getExportIntegrationMessageHTML, diff --git a/src/libs/ReportUtils.ts b/src/libs/ReportUtils.ts index 4072f5c07e00..41a640d13005 100644 --- a/src/libs/ReportUtils.ts +++ b/src/libs/ReportUtils.ts @@ -59,6 +59,7 @@ import type { Transaction, TransactionViolation, TransactionViolations, + VisibleReportActionsDerivedValue, } from '@src/types/onyx'; import type {ReportTransactionsAndViolations} from '@src/types/onyx/DerivedValues'; import type {Attendee, Participant} from '@src/types/onyx/IOU'; @@ -266,6 +267,7 @@ import { isRenamedAction, isReopenedAction, isReportActionAttachment, + isReportActionVisible, isReportPreviewAction, isRetractedAction, isReversedTransaction, @@ -280,7 +282,6 @@ import { isTripPreview, isUnapprovedAction, isWhisperAction, - shouldReportActionBeVisible, wasActionTakenByCurrentUser, } from './ReportActionsUtils'; import type {LastVisibleMessage} from './ReportActionsUtils'; @@ -7033,7 +7034,7 @@ function getMovedTransactionMessage(translate: LocalizedTranslate, action: Repor // This will be fixed as follow up https://github.com/Expensify/App/pull/75357 // eslint-disable-next-line @typescript-eslint/no-deprecated - const reportName = getReportName(report) ?? report?.reportName ?? ''; + const reportName = Parser.htmlToText(getReportName(report) ?? report?.reportName ?? ''); const reportUrl = getReportURLForCurrentContext(report?.reportID); if (typeof fromReportID === 'undefined') { return translate('iou.movedTransactionTo', { @@ -7055,7 +7056,7 @@ function getUnreportedTransactionMessage(translate: LocalizedTranslate, action: // This will be fixed as follow up https://github.com/Expensify/App/pull/75357 // eslint-disable-next-line @typescript-eslint/no-deprecated - const reportName = getReportName(fromReport) ?? fromReport?.reportName ?? ''; + const reportName = Parser.htmlToText(getReportName(fromReport) ?? fromReport?.reportName ?? ''); let reportUrl = getReportURLForCurrentContext(fromReportID); @@ -9156,14 +9157,23 @@ function isReportNotFound(report: OnyxEntry): boolean { /** * Check if the report is the parent report of the currently viewed report or at least one child report has report action */ -function shouldHideReport(report: OnyxEntry, currentReportId: string | undefined, isReportArchived: boolean | undefined): boolean { +function shouldHideReport( + report: OnyxEntry, + currentReportId: string | undefined, + isReportArchived: boolean | undefined, + visibleReportActionsData?: VisibleReportActionsDerivedValue, +): boolean { const currentReport = getReportOrDraftReport(currentReportId); const parentReport = getParentReport(!isEmptyObject(currentReport) ? currentReport : undefined); const reportActions = allReportActions?.[`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report?.reportID}`] ?? {}; - const isChildReportHasComment = Object.values(reportActions ?? {})?.some( - (reportAction) => - (reportAction?.childVisibleActionCount ?? 0) > 0 && shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction(report, isReportArchived)), - ); + const reportID = report?.reportID; + const isChildReportHasComment = + !!reportID && + Object.values(reportActions ?? {})?.some( + (reportAction) => + (reportAction?.childVisibleActionCount ?? 0) > 0 && + isReportActionVisible(reportAction, reportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData), + ); return parentReport?.reportID !== report?.reportID && !isChildReportHasComment; } @@ -10191,7 +10201,13 @@ function isMoneyRequestReportPendingDeletion(reportOrID: OnyxEntry | str return parentReportAction?.pendingAction === CONST.RED_BRICK_ROAD_PENDING_ACTION.DELETE; } -function navigateToLinkedReportAction(ancestor: Ancestor, isInNarrowPaneModal: boolean, canUserPerformWrite: boolean | undefined, isOffline: boolean) { +function navigateToLinkedReportAction( + ancestor: Ancestor, + isInNarrowPaneModal: boolean, + canUserPerformWrite: boolean | undefined, + isOffline: boolean, + visibleReportActionsData?: VisibleReportActionsDerivedValue, +) { if (isInNarrowPaneModal) { Navigation.navigate( ROUTES.SEARCH_REPORT.getRoute({ @@ -10206,7 +10222,8 @@ function navigateToLinkedReportAction(ancestor: Ancestor, isInNarrowPaneModal: b // Pop the thread report screen before navigating to the chat report. Navigation.goBack(ROUTES.REPORT_WITH_ID.getRoute(ancestor.report.reportID)); - const isVisibleAction = shouldReportActionBeVisible(ancestor.reportAction, ancestor.reportAction.reportActionID, canUserPerformWrite); + const reportID = ancestor.report.reportID; + const isVisibleAction = !!reportID && isReportActionVisible(ancestor.reportAction, reportID, canUserPerformWrite, visibleReportActionsData); if (isVisibleAction && !isOffline) { // Pop the chat report screen before navigating to the linked report action. diff --git a/src/libs/SearchUIUtils.ts b/src/libs/SearchUIUtils.ts index 8f0d14c38f9f..37b5d7527a9b 100644 --- a/src/libs/SearchUIUtils.ts +++ b/src/libs/SearchUIUtils.ts @@ -108,9 +108,9 @@ import { isDeletedAction, isHoldAction, isMoneyRequestAction, + isReportActionVisible, isResolvedActionableWhisper, isWhisperActionTargetedToOthers, - shouldReportActionBeVisible, } from './ReportActionsUtils'; import {isExportAction} from './ReportPrimaryActionUtils'; import { @@ -453,6 +453,7 @@ type GetSectionsParams = { isOffline?: boolean; cardFeeds?: OnyxCollection; allTransactionViolations?: OnyxCollection; + visibleReportActionsData?: OnyxTypes.VisibleReportActionsDerivedValue; allReportMetadata: OnyxCollection; }; @@ -1845,7 +1846,7 @@ function createAndOpenSearchTransactionThread( * * Do not use directly, use only via `getSections()` facade. */ -function getReportActionsSections(data: OnyxTypes.SearchResults['data']): [ReportActionListItemType[], number] { +function getReportActionsSections(data: OnyxTypes.SearchResults['data'], visibleReportActionsData?: OnyxTypes.VisibleReportActionsDerivedValue): [ReportActionListItemType[], number] { const reportActionItems: ReportActionListItemType[] = []; const transactions = Object.keys(data) @@ -1864,11 +1865,13 @@ function getReportActionsSections(data: OnyxTypes.SearchResults['data']): [Repor for (const key in data) { if (isReportActionEntry(key)) { + const reportIDFromKey = key.replace(ONYXKEYS.COLLECTION.REPORT_ACTIONS, ''); const reportActions = Object.values(data[key]); n += reportActions.length; for (const reportAction of reportActions) { + const reportID = reportAction.reportID ?? reportIDFromKey; const from = reportAction.accountID ? (data.personalDetailsList?.[reportAction.accountID] ?? emptyPersonalDetails) : emptyPersonalDetails; - const report = data[`${ONYXKEYS.COLLECTION.REPORT}${reportAction.reportID}`] ?? {}; + const report = data[`${ONYXKEYS.COLLECTION.REPORT}${reportID}`] ?? {}; const policy = data[`${ONYXKEYS.COLLECTION.POLICY}${report.policyID}`] ?? {}; const originalMessage = isMoneyRequestAction(reportAction) ? getOriginalMessage(reportAction) : undefined; const isSendingMoney = isMoneyRequestAction(reportAction) && originalMessage?.type === CONST.IOU.REPORT_ACTION_TYPE.PAY && originalMessage?.IOUDetails; @@ -1876,7 +1879,8 @@ function getReportActionsSections(data: OnyxTypes.SearchResults['data']): [Repor const invoiceReceiverPolicy: OnyxTypes.Policy | undefined = report?.invoiceReceiver?.type === CONST.REPORT.INVOICE_RECEIVER_TYPE.BUSINESS ? data[`${ONYXKEYS.COLLECTION.POLICY}${report.invoiceReceiver.policyID}`] : undefined; if ( - !shouldReportActionBeVisible(reportAction, reportAction.reportActionID, canUserPerformWriteAction(report, isReportArchived)) || + !reportID || + !isReportActionVisible(reportAction, reportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData) || isDeletedAction(reportAction) || isResolvedActionableWhisper(reportAction) || reportAction.actionName === CONST.REPORT.ACTIONS.TYPE.CLOSED || @@ -2649,10 +2653,11 @@ function getSections({ isOffline, cardFeeds, allTransactionViolations, + visibleReportActionsData, allReportMetadata, }: GetSectionsParams) { if (type === CONST.SEARCH.DATA_TYPES.CHAT) { - return getReportActionsSections(data); + return getReportActionsSections(data, visibleReportActionsData); } if (type === CONST.SEARCH.DATA_TYPES.TASK) { return getTaskSections(data, formatPhoneNumber, archivedReportsIDList); diff --git a/src/libs/SidebarUtils.ts b/src/libs/SidebarUtils.ts index d9147fe48b5e..2081c685aa56 100644 --- a/src/libs/SidebarUtils.ts +++ b/src/libs/SidebarUtils.ts @@ -5,7 +5,17 @@ import type {LocaleContextProps, LocalizedTranslate} from '@components/LocaleCon import type {PartialPolicyForSidebar, ReportsToDisplayInLHN} from '@hooks/useSidebarOrderedReports'; import CONST from '@src/CONST'; import ONYXKEYS from '@src/ONYXKEYS'; -import type {Card, PersonalDetails, PersonalDetailsList, ReportActions, ReportAttributesDerivedValue, ReportNameValuePairs, Transaction, TransactionViolation} from '@src/types/onyx'; +import type { + Card, + PersonalDetails, + PersonalDetailsList, + ReportActions, + ReportAttributesDerivedValue, + ReportNameValuePairs, + Transaction, + TransactionViolation, + VisibleReportActionsDerivedValue, +} from '@src/types/onyx'; import type Beta from '@src/types/onyx/Beta'; import type {ReportAttributes} from '@src/types/onyx/DerivedValues'; import type {Errors} from '@src/types/onyx/OnyxCommon'; @@ -653,6 +663,7 @@ function getOptionData({ movedFromReport, movedToReport, currentUserAccountID, + visibleReportActionsData, }: { report: OnyxEntry; oneTransactionThreadReport: OnyxEntry; @@ -672,6 +683,7 @@ function getOptionData({ movedFromReport?: OnyxEntry; movedToReport?: OnyxEntry; currentUserAccountID: number; + visibleReportActionsData?: VisibleReportActionsDerivedValue; }): OptionData | undefined { // When a user signs out, Onyx is cleared. Due to the lazy rendering with a virtual list, it's possible for // this method to be called after the Onyx data has been cleared out. In that case, it's fine to do @@ -802,7 +814,18 @@ function getOptionData({ const lastActorDisplayName = getLastActorDisplayName(lastActorDetails, currentUserAccountID); let lastMessageTextFromReport = lastMessageTextFromReportProp; if (!lastMessageTextFromReport) { - lastMessageTextFromReport = getLastMessageTextForReport({translate, report, lastActorDetails, movedFromReport, movedToReport, policy, isReportArchived, currentUserAccountID}); + lastMessageTextFromReport = getLastMessageTextForReport({ + translate, + report, + lastActorDetails, + movedFromReport, + movedToReport, + policy, + isReportArchived, + visibleReportActionsDataParam: visibleReportActionsData, + lastAction, + currentUserAccountID, + }); } // We need to remove sms domain in case the last message text has a phone number mention with sms domain. @@ -954,7 +977,8 @@ function getOptionData({ result.alternateText = getCardIssuedMessage({reportAction: lastAction, expensifyCard: card, translate}); } else if (lastAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.REPORT_PREVIEW && lastActorDisplayName && lastMessageTextFromReport) { const displayName = - (lastMessageTextFromReport.length > 0 && getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID, personalDetails)) || + (lastMessageTextFromReport.length > 0 && + getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID, personalDetails, visibleReportActionsData, lastAction)) || lastActorDisplayName; result.alternateText = formatReportLastMessageText(`${displayName}: ${lastMessageText}`); } else if (lastAction && isOldDotReportAction(lastAction)) { @@ -1023,7 +1047,8 @@ function getOptionData({ } if (shouldShowLastActorDisplayName(report, lastActorDetails, lastAction, currentUserAccountID) && !isReportArchived) { const displayName = - (lastMessageTextFromReport.length > 0 && getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID, personalDetails)) || + (lastMessageTextFromReport.length > 0 && + getLastActorDisplayNameFromLastVisibleActions(report, lastActorDetails, currentUserAccountID, personalDetails, visibleReportActionsData, lastAction)) || lastActorDisplayName; result.alternateText = `${displayName}: ${formatReportLastMessageText(lastMessageText)}`; } else { diff --git a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts index af219af05716..716256354fc2 100644 --- a/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts +++ b/src/libs/actions/OnyxDerived/ONYX_DERIVED_VALUES.ts @@ -5,6 +5,7 @@ import nonPersonalAndWorkspaceCardListConfig from './configs/nonPersonalAndWorks 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'; /** @@ -15,6 +16,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, [ONYXKEYS.DERIVED.NON_PERSONAL_AND_WORKSPACE_CARD_LIST]: nonPersonalAndWorkspaceCardListConfig, [ONYXKEYS.DERIVED.CARD_FEED_ERRORS]: cardFeedErrorsConfig, } as const satisfies { diff --git a/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts new file mode 100644 index 000000000000..e69e5e661c11 --- /dev/null +++ b/src/libs/actions/OnyxDerived/configs/visibleReportActions.ts @@ -0,0 +1,151 @@ +import type {OnyxEntry} from 'react-native-onyx'; +import {isActionableWhisperRequiringWritePermission, isConciergeCategoryOptions, isMovedTransactionAction, shouldReportActionBeVisible} from '@libs/ReportActionsUtils'; +import createOnyxDerivedValueConfig from '@userActions/OnyxDerived/createOnyxDerivedValueConfig'; +import CONST from '@src/CONST'; +import ONYXKEYS from '@src/ONYXKEYS'; +import type {ReportAction, ReportActions} from '@src/types/onyx'; +import type {VisibleReportActionsDerivedValue} from '@src/types/onyx/DerivedValues'; + +function getOrCreateReportVisibilityRecord(result: VisibleReportActionsDerivedValue, reportID: string): Record { + if (!result[reportID]) { + // eslint-disable-next-line no-param-reassign + result[reportID] = {}; + } + return result[reportID]; +} + +function doesActionDependOnReportExistence(action: ReportAction): boolean { + const isUnreportedTransaction = action.actionName === CONST.REPORT.ACTIONS.TYPE.UNREPORTED_TRANSACTION; + const isMovedTransaction = isMovedTransactionAction(action as OnyxEntry); + + return isUnreportedTransaction || isMovedTransaction; +} + +/** + * Returns true if the action's visibility depends on runtime context that can't be cached, + * such as write permissions or policy settings. + */ +function shouldSkipCachingAction(action: ReportAction): boolean { + return isActionableWhisperRequiringWritePermission(action) || isConciergeCategoryOptions(action); +} + +export default createOnyxDerivedValueConfig({ + key: ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, + // Note: REPORT and SESSION dependencies are needed to trigger recompute when reports change + // (for UNREPORTED_TRANSACTION/MOVED_TRANSACTION visibility) or when user changes (for whisper targeting). + // shouldReportActionBeVisible uses global Onyx-connected variables internally. + dependencies: [ONYXKEYS.COLLECTION.REPORT_ACTIONS, ONYXKEYS.COLLECTION.REPORT, ONYXKEYS.SESSION], + compute: ([allReportActions], {sourceValues, currentValue}): VisibleReportActionsDerivedValue => { + if (!allReportActions) { + return {}; + } + + 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) { + if (shouldSkipCachingAction(action)) { + continue; + } + reportVisibility[action.reportActionID] = shouldReportActionBeVisible(action, actionID); + } + } + } + + return result; + } + + const result: VisibleReportActionsDerivedValue = currentValue ? {...currentValue} : {}; + + // Reports changed - recompute actions that depend on report existence + if (reportUpdates) { + 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)) { + if (shouldSkipCachingAction(action)) { + delete reportVisibility[actionID]; + delete reportVisibility[action.reportActionID]; + continue; + } + reportVisibility[action.reportActionID] = shouldReportActionBeVisible(action, actionID); + if (actionID !== action.reportActionID) { + delete reportVisibility[actionID]; + } + } + } + } + + if (!reportActionsUpdates) { + return result; + } + } + + const reportActionsToProcess = reportActionsUpdates ? Object.keys(reportActionsUpdates) : Object.keys(allReportActions); + + for (const reportActionsKey of reportActionsToProcess) { + const reportActions: OnyxEntry = 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 actionIDsToProcess = specificUpdates ? Object.keys(specificUpdates) : Object.keys(reportActions); + + for (const actionID of actionIDsToProcess) { + if (specificUpdates && specificUpdates[actionID] === null) { + delete reportVisibility[actionID]; + continue; + } + + const action = reportActions[actionID]; + if (!action) { + delete reportVisibility[actionID]; + continue; + } + + if (shouldSkipCachingAction(action)) { + delete reportVisibility[actionID]; + delete reportVisibility[action.reportActionID]; + continue; + } + + reportVisibility[action.reportActionID] = shouldReportActionBeVisible(action, actionID); + if (actionID !== action.reportActionID) { + delete reportVisibility[actionID]; + } + } + } + + return result; + }, +}); diff --git a/src/libs/actions/Report/index.ts b/src/libs/actions/Report/index.ts index 8e8df7fe6d6f..2cd1e377e1c1 100644 --- a/src/libs/actions/Report/index.ts +++ b/src/libs/actions/Report/index.ts @@ -220,6 +220,7 @@ import type { ReportUserIsTyping, Transaction, TransactionViolations, + VisibleReportActionsDerivedValue, } from '@src/types/onyx'; import type {Decision} from '@src/types/onyx/OriginalMessage'; import type {CurrentUserPersonalDetails, Timezone} from '@src/types/onyx/PersonalDetails'; @@ -2088,6 +2089,7 @@ function deleteReportComment( isReportArchived: boolean | undefined, isOriginalReportArchived: boolean | undefined, currentEmail: string, + visibleReportActionsDataParam?: VisibleReportActionsDerivedValue, ) { const originalReportID = getOriginalReportID(reportID, reportAction); const reportActionID = reportAction.reportActionID; @@ -2134,7 +2136,7 @@ function deleteReportComment( (action) => action.reportActionID !== reportAction.reportActionID && ReportActionsUtils.didMessageMentionCurrentUser(action, currentEmail) && - ReportActionsUtils.shouldReportActionBeVisible(action, action.reportActionID), + ReportActionsUtils.isReportActionVisible(action, reportID, undefined, visibleReportActionsDataParam), ); optimisticReport.lastMentionedTime = latestMentionedReportAction?.created ?? null; } @@ -2290,6 +2292,7 @@ function editReportComment( isOriginalParentReportArchived: boolean | undefined, currentUserLogin: string, videoAttributeCache?: Record, + visibleReportActionsDataParam?: VisibleReportActionsDerivedValue, ) { const originalReportID = originalReport?.reportID; if (!originalReportID || !originalReportAction) { @@ -2322,7 +2325,7 @@ function editReportComment( // Delete the comment if it's empty if (!htmlForNewComment) { - deleteReportComment(originalReportID, originalReportAction, ancestors, isOriginalReportArchived, isOriginalParentReportArchived, currentUserLogin); + deleteReportComment(originalReportID, originalReportAction, ancestors, isOriginalReportArchived, isOriginalParentReportArchived, currentUserLogin, visibleReportActionsDataParam); return; } diff --git a/src/libs/registerPaginationConfig.ts b/src/libs/registerPaginationConfig.ts index a05c1474a706..47940e8cb2c8 100644 --- a/src/libs/registerPaginationConfig.ts +++ b/src/libs/registerPaginationConfig.ts @@ -46,7 +46,7 @@ registerPaginationConfig({ const reportNameValuePairs = allReportNameValuePairs?.[`${ONYXKEYS.COLLECTION.REPORT_NAME_VALUE_PAIRS}${reportID}`]; const isReportArchived = !!reportNameValuePairs?.private_isArchived; const canUserPerformWriteAction = canUserPerformWriteActionReportUtils(report, isReportArchived); - return getSortedReportActionsForDisplay(reportActions, canUserPerformWriteAction, true); + return getSortedReportActionsForDisplay(reportActions, canUserPerformWriteAction, true, undefined, reportID); }, getItemID: (reportAction) => reportAction.reportActionID, }); diff --git a/src/pages/Debug/Report/DebugReportActions.tsx b/src/pages/Debug/Report/DebugReportActions.tsx index d1662bdba411..c211a477a21f 100644 --- a/src/pages/Debug/Report/DebugReportActions.tsx +++ b/src/pages/Debug/Report/DebugReportActions.tsx @@ -45,9 +45,9 @@ function DebugReportActions({reportID}: DebugReportActionsProps) { const getSortedAllReportActionsSelector = useCallback( (allReportActions: OnyxEntry): ReportAction[] => { - return getSortedReportActionsForDisplay(allReportActions, ifUserCanPerformWriteAction, true); + return getSortedReportActionsForDisplay(allReportActions, ifUserCanPerformWriteAction, true, undefined, reportID); }, - [ifUserCanPerformWriteAction], + [ifUserCanPerformWriteAction, reportID], ); const [sortedAllReportActions] = useOnyx( diff --git a/src/pages/Share/ShareTab.tsx b/src/pages/Share/ShareTab.tsx index 7d71ee819fdc..f454bd06c794 100644 --- a/src/pages/Share/ShareTab.tsx +++ b/src/pages/Share/ShareTab.tsx @@ -51,6 +51,7 @@ function ShareTab({ref}: ShareTabProps) { const [loginList] = useOnyx(ONYXKEYS.LOGIN_LIST, {canBeMissing: true}); const [draftComments] = useOnyx(ONYXKEYS.COLLECTION.REPORT_DRAFT_COMMENT, {canBeMissing: true}); const [nvpDismissedProductTraining] = useOnyx(ONYXKEYS.NVP_DISMISSED_PRODUCT_TRAINING, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const currentUserPersonalDetails = useCurrentUserPersonalDetails(); const currentUserAccountID = currentUserPersonalDetails.accountID; const currentUserEmail = currentUserPersonalDetails.email ?? ''; @@ -81,10 +82,23 @@ function ShareTab({ref}: ShareTabProps) { includeUserToInvite: true, countryCode, loginList, + visibleReportActionsData, currentUserAccountID, currentUserEmail, }); - }, [areOptionsInitialized, options, draftComments, nvpDismissedProductTraining, betas, textInputValue, countryCode, loginList, currentUserAccountID, currentUserEmail]); + }, [ + areOptionsInitialized, + options, + draftComments, + nvpDismissedProductTraining, + betas, + textInputValue, + countryCode, + loginList, + visibleReportActionsData, + currentUserAccountID, + currentUserEmail, + ]); const recentReportsOptions = useMemo(() => { if (textInputValue.trim() === '') { diff --git a/src/pages/inbox/ReportScreen.tsx b/src/pages/inbox/ReportScreen.tsx index 0b2df4f01a01..3bc67be486d3 100644 --- a/src/pages/inbox/ReportScreen.tsx +++ b/src/pages/inbox/ReportScreen.tsx @@ -59,10 +59,10 @@ import { isCreatedAction, isDeletedParentAction, isMoneyRequestAction, + isReportActionVisible, isSentMoneyReportAction, isTransactionThread, isWhisperAction, - shouldReportActionBeVisible, } from '@libs/ReportActionsUtils'; import { canEditReportAction, @@ -304,6 +304,7 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr const [currentUserAccountID = -1] = useOnyx(ONYXKEYS.SESSION, {selector: accountIDSelector, canBeMissing: false}); const [isLoadingApp] = useOnyx(ONYXKEYS.IS_LOADING_APP, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const {reportActions: unfilteredReportActions, linkedAction, sortedAllReportActions, hasNewerActions, hasOlderActions} = usePaginatedReportActions(reportID, reportActionIDFromRoute); // wrapping in useMemo because this is array operation and can cause performance issues const reportActions = useMemo(() => getFilteredReportActionsForReportView(unfilteredReportActions), [unfilteredReportActions]); @@ -480,10 +481,16 @@ function ReportScreen({route, navigation, isInSidePanel = false}: ReportScreenPr const isReportArchived = useReportIsArchived(report?.reportID); const {isEditingDisabled, isCurrentReportLoadedFromOnyx} = useIsReportReadyToDisplay(report, reportIDFromRoute, isReportArchived); - const isLinkedActionDeleted = useMemo( - () => !!linkedAction && !shouldReportActionBeVisible(linkedAction, linkedAction.reportActionID, canUserPerformWriteAction(report, isReportArchived)), - [linkedAction, report, isReportArchived], - ); + const isLinkedActionDeleted = useMemo(() => { + if (!linkedAction) { + return false; + } + const actionReportID = linkedAction.reportID ?? reportID; + if (!actionReportID) { + return true; + } + return !isReportActionVisible(linkedAction, actionReportID, canUserPerformWriteAction(report, isReportArchived), visibleReportActionsData); + }, [linkedAction, report, isReportArchived, reportID, visibleReportActionsData]); const prevIsLinkedActionDeleted = usePrevious(linkedAction ? isLinkedActionDeleted : undefined); diff --git a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx index a07efba6485e..8bbd0b46bf17 100644 --- a/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx +++ b/src/pages/inbox/report/ContextMenu/PopoverReportActionContextMenu.tsx @@ -86,6 +86,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro const [shouldSwitchPositionIfOverflow, setShouldSwitchPositionIfOverflow] = useState(false); const [isWithoutOverlay, setIsWithoutOverlay] = useState(true); const [allTransactionViolations] = useOnyx(ONYXKEYS.COLLECTION.TRANSACTION_VIOLATIONS, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const contentRef = useRef(null); const anchorRef = useRef(null); @@ -373,7 +374,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro } else if (reportAction) { // eslint-disable-next-line @typescript-eslint/no-deprecated InteractionManager.runAfterInteractions(() => { - deleteReportComment(reportIDRef.current, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived, email ?? ''); + deleteReportComment(reportIDRef.current, reportAction, ancestorsRef.current, isReportArchived, isOriginalReportArchived, email ?? '', visibleReportActionsData ?? undefined); }); } @@ -394,6 +395,7 @@ function PopoverReportActionContextMenu({ref}: PopoverReportActionContextMenuPro isOriginalReportArchived, allTransactionViolations, bankAccountList, + visibleReportActionsData, currentUserAccountID, ]); diff --git a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx index 5c624142612f..7a600cce5a0f 100644 --- a/src/pages/inbox/report/ReportActionItemMessageEdit.tsx +++ b/src/pages/inbox/report/ReportActionItemMessageEdit.tsx @@ -151,6 +151,7 @@ function ReportActionItemMessageEdit({ // The ref to check whether the comment saving is in progress const isCommentPendingSaved = useRef(false); const [originalReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${originalReportID}`, {canBeMissing: true}); + const [visibleReportActionsData] = useOnyx(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, {canBeMissing: true}); const isOriginalReportArchived = useReportIsArchived(originalReportID); const originalParentReportID = getOriginalReportID(originalReportID, action); const isOriginalParentReportArchived = useReportIsArchived(originalParentReportID); @@ -325,6 +326,7 @@ function ReportActionItemMessageEdit({ isOriginalParentReportArchived, email ?? '', Object.fromEntries(draftMessageVideoAttributeCache), + visibleReportActionsData ?? undefined, ); deleteDraft(); }, [ @@ -339,6 +341,7 @@ function ReportActionItemMessageEdit({ isOriginalParentReportArchived, debouncedValidateCommentMaxLength, email, + visibleReportActionsData, ]); /** diff --git a/src/pages/inbox/report/ReportActionsView.tsx b/src/pages/inbox/report/ReportActionsView.tsx index a1e2c00310d7..e78b800563ed 100755 --- a/src/pages/inbox/report/ReportActionsView.tsx +++ b/src/pages/inbox/report/ReportActionsView.tsx @@ -29,7 +29,7 @@ import { isDeletedParentAction, isIOUActionMatchingTransactionList, isMoneyRequestAction, - shouldReportActionBeVisible, + isReportActionVisible, } from '@libs/ReportActionsUtils'; import {buildOptimisticCreatedReportAction, buildOptimisticIOUReportAction, canUserPerformWriteAction, isInvoiceReport, isMoneyRequestReport} from '@libs/ReportUtils'; import markOpenReportEnd from '@libs/telemetry/markOpenReportEnd'; @@ -87,9 +87,9 @@ function ReportActionsView({ const getTransactionThreadReportActions = useCallback( (reportActions: OnyxEntry): OnyxTypes.ReportAction[] => { - return getSortedReportActionsForDisplay(reportActions, canPerformWriteAction, true); + return getSortedReportActionsForDisplay(reportActions, canPerformWriteAction, true, undefined, transactionThreadReportID ?? undefined); }, - [canPerformWriteAction], + [canPerformWriteAction, transactionThreadReportID], ); const [transactionThreadReportActions] = useOnyx( @@ -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); @@ -218,13 +219,26 @@ 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; + if (!isReportActionVisible(reportAction, actionReportID, canPerformWriteAction, visibleReportActionsData)) { + return false; + } + + if (!isIOUActionMatchingTransactionList(reportAction, reportTransactionIDs)) { + return false; + } + + return true; + }), + [reportActions, isOffline, canPerformWriteAction, reportTransactionIDs, visibleReportActionsData, reportID], ); const newestReportAction = useMemo(() => reportActions?.at(0), [reportActions]); @@ -309,7 +323,8 @@ function ReportActionsView({ return ; } - if (!isReportTransactionThread && isMissingReportActions) { + const hasDerivedValueTimingIssue = reportActions.length > 0 && isMissingReportActions; + if (hasDerivedValueTimingIssue || (!isReportTransactionThread && isMissingReportActions)) { return ; } diff --git a/src/types/onyx/DerivedValues.ts b/src/types/onyx/DerivedValues.ts index 0dc59c2f4dc5..7dfd3b59b897 100644 --- a/src/types/onyx/DerivedValues.ts +++ b/src/types/onyx/DerivedValues.ts @@ -73,6 +73,11 @@ type ReportTransactionsAndViolationsDerivedValue = Record>; +/** + * The derived value for visible report actions. + */ +type VisibleReportActionsDerivedValue = Record>; + /** * The errors of a card. */ @@ -216,6 +221,7 @@ export type { ReportTransactionsAndViolationsDerivedValue, ReportTransactionsAndViolations, OutstandingReportsByPolicyIDDerivedValue, + VisibleReportActionsDerivedValue, NonPersonalAndWorkspaceCardListDerivedValue, CardFeedErrorsDerivedValue, AllCardFeedErrorsMap, diff --git a/src/types/onyx/index.ts b/src/types/onyx/index.ts index c5f2a6916fc6..9401d36d54b7 100644 --- a/src/types/onyx/index.ts +++ b/src/types/onyx/index.ts @@ -46,6 +46,7 @@ import type { OutstandingReportsByPolicyIDDerivedValue, ReportAttributesDerivedValue, ReportTransactionsAndViolationsDerivedValue, + VisibleReportActionsDerivedValue, } from './DerivedValues'; import type DismissedProductTraining from './DismissedProductTraining'; import type DismissedReferralBanners from './DismissedReferralBanners'; @@ -330,6 +331,7 @@ export type { LastSearchParams, ReportTransactionsAndViolationsDerivedValue, OutstandingReportsByPolicyIDDerivedValue, + VisibleReportActionsDerivedValue, NonPersonalAndWorkspaceCardListDerivedValue, CardFeedErrorsDerivedValue, ScheduleCallDraft, diff --git a/tests/actions/ReportTest.ts b/tests/actions/ReportTest.ts index c1a1556a688b..5ce96d5e913f 100644 --- a/tests/actions/ReportTest.ts +++ b/tests/actions/ReportTest.ts @@ -490,6 +490,7 @@ describe('actions/Report', () => { person: [{type: 'TEXT', style: 'strong', text: 'Test User'}], shouldShow: true, created: DateUtils.getDBTime(Date.now() - 3), + reportID: REPORT_ID, }; const optimisticReportActions: OnyxUpdate = { diff --git a/tests/ui/components/LHNOptionsListTest.tsx b/tests/ui/components/LHNOptionsListTest.tsx index 76b147044f4b..2dc57b168e9a 100644 --- a/tests/ui/components/LHNOptionsListTest.tsx +++ b/tests/ui/components/LHNOptionsListTest.tsx @@ -210,12 +210,18 @@ describe('LHNOptionsList', () => { await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: true}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policy); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - [submittedAction.reportActionID]: submittedAction, - }); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, { pendingExpenseAction: CONST.EXPENSE_PENDING_ACTION.SUBMIT, }); + + await Onyx.merge(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, { + [reportID]: { + [submittedAction.reportActionID]: true, + }, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { + [submittedAction.reportActionID]: submittedAction, + }); }); // When the LHNOptionsList is rendered @@ -261,12 +267,18 @@ describe('LHNOptionsList', () => { await Onyx.merge(ONYXKEYS.NETWORK, {isOffline: false}); await Onyx.merge(`${ONYXKEYS.COLLECTION.POLICY}${policyID}`, policy); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${reportID}`, report); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { - [commentAction.reportActionID]: commentAction, - }); await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_METADATA}${reportID}`, { pendingExpenseAction: CONST.EXPENSE_PENDING_ACTION.SUBMIT, }); + + await Onyx.merge(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, { + [reportID]: { + [commentAction.reportActionID]: true, + }, + }); + await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${reportID}`, { + [commentAction.reportActionID]: commentAction, + }); }); // When the LHNOptionsList is rendered diff --git a/tests/unit/OptionsListUtilsTest.tsx b/tests/unit/OptionsListUtilsTest.tsx index 54fa043dc6fa..725011d14142 100644 --- a/tests/unit/OptionsListUtilsTest.tsx +++ b/tests/unit/OptionsListUtilsTest.tsx @@ -3188,17 +3188,16 @@ describe('OptionsListUtils', () => { }); describe('getLastMessageTextForReport', () => { - describe('REPORT_PREVIEW action', () => { - it('should show report preview message for non-policy expense chat', async () => { - const report: Report = { - ...createRandomReport(0, undefined), - isOwnPolicyExpenseChat: false, - }; + describe('getReportPreviewMessage', () => { + it('should format report preview message correctly for non-policy expense chat with IOU action', async () => { const iouReport: Report = { ...createRandomReport(1, undefined), isOwnPolicyExpenseChat: false, type: CONST.REPORT.TYPE.IOU, isWaitingOnBankAccount: false, + currency: CONST.CURRENCY.USD, + total: 100, + unheldTotal: 100, }; const reportPreviewAction: ReportAction = { ...createRandomReportAction(1), @@ -3222,6 +3221,7 @@ describe('OptionsListUtils', () => { }; const iouAction: ReportAction = { ...createRandomReportAction(2), + reportID: iouReport.reportID, actionName: CONST.REPORT.ACTIONS.TYPE.IOU, message: [{type: 'COMMENT', text: ''}], originalMessage: { @@ -3230,24 +3230,15 @@ describe('OptionsListUtils', () => { }, shouldShow: true, }; - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, { - [reportPreviewAction.reportActionID]: reportPreviewAction, - }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReport.reportID}`, { - [iouAction.reportActionID]: iouAction, - }); - await Onyx.merge(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); - const lastMessage = getLastMessageTextForReport({ - translate: translateLocal, - report, - lastActorDetails: null, - isReportArchived: false, - currentUserAccountID: CURRENT_USER_ACCOUNT_ID, - }); + + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReport.reportID}`, iouReport); + await Onyx.set(`${ONYXKEYS.COLLECTION.TRANSACTION}${transaction.transactionID}`, transaction); + await waitForBatchedUpdates(); + + // Test getReportPreviewMessage directly - this is the function responsible for formatting the message const reportPreviewMessage = getReportPreviewMessage(iouReport, iouAction, true, false, null, true, reportPreviewAction); - const expected = formatReportLastMessageText(Parser.htmlToText(reportPreviewMessage)); - expect(lastMessage).toBe(expected); + const formattedMessage = formatReportLastMessageText(Parser.htmlToText(reportPreviewMessage)); + expect(formattedMessage).toBe('$1.00 for A A A'); }); }); it('MOVED_TRANSACTION action', async () => { @@ -3300,6 +3291,7 @@ describe('OptionsListUtils', () => { report, lastActorDetails: null, isReportArchived: false, + visibleReportActionsDataParam: {}, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, }); expect(lastMessage).toBe(Parser.htmlToText(translate(CONST.LOCALES.EN, 'iou.automaticallySubmitted'))); @@ -3325,6 +3317,7 @@ describe('OptionsListUtils', () => { report, lastActorDetails: null, isReportArchived: false, + visibleReportActionsDataParam: {}, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, }); expect(lastMessage).toBe(Parser.htmlToText(translate(CONST.LOCALES.EN, 'iou.automaticallyApproved'))); @@ -3350,6 +3343,7 @@ describe('OptionsListUtils', () => { report, lastActorDetails: null, isReportArchived: false, + visibleReportActionsDataParam: {}, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, }); expect(lastMessage).toBe(Parser.htmlToText(translate(CONST.LOCALES.EN, 'iou.automaticallyForwarded'))); @@ -3372,6 +3366,7 @@ describe('OptionsListUtils', () => { report, lastActorDetails: null, isReportArchived: false, + visibleReportActionsDataParam: {}, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, }); expect(lastMessage).toBe(Parser.htmlToText(translate(CONST.LOCALES.EN, 'workspaceActions.forcedCorporateUpgrade'))); @@ -3483,6 +3478,7 @@ describe('OptionsListUtils', () => { report, lastActorDetails: null, isReportArchived: false, + visibleReportActionsDataParam: {}, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, }); expect(result).toBe(expectedVisibleText); @@ -3534,6 +3530,7 @@ describe('OptionsListUtils', () => { isReportArchived: false, policy, reportMetadata, + visibleReportActionsDataParam: {}, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, }); expect(lastMessage).toBe(translate(CONST.LOCALES.EN, 'iou.queuedToSubmitViaDEW')); @@ -3566,6 +3563,7 @@ describe('OptionsListUtils', () => { report, lastActorDetails: null, isReportArchived: false, + visibleReportActionsDataParam: {}, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, }); expect(lastMessage).toBe(customErrorMessage); @@ -3595,6 +3593,7 @@ describe('OptionsListUtils', () => { report, lastActorDetails: null, isReportArchived: false, + visibleReportActionsDataParam: {}, currentUserAccountID: CURRENT_USER_ACCOUNT_ID, }); expect(lastMessage).toBe(translate(CONST.LOCALES.EN, 'iou.error.genericCreateFailureMessage')); diff --git a/tests/unit/ReportActionsUtilsTest.ts b/tests/unit/ReportActionsUtilsTest.ts index 24a06868b876..35ce577d3bb7 100644 --- a/tests/unit/ReportActionsUtilsTest.ts +++ b/tests/unit/ReportActionsUtilsTest.ts @@ -580,6 +580,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-13 22:27:01.825', reportActionID: '8401445780099176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', @@ -596,6 +597,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-12 22:27:01.825', reportActionID: '6401435781022176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, originalMessage: { html: 'Hello world', @@ -612,6 +614,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-11 22:27:01.825', reportActionID: '2962390724708756', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.IOU, originalMessage: { amount: 0, @@ -629,6 +632,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-10 22:27:01.825', reportActionID: '1609646094152486', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.RENAMED, originalMessage: { html: 'Hello world', @@ -647,6 +651,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-09 22:27:01.825', reportActionID: '8049485084562457', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.POLICY_CHANGE_LOG.UPDATE_FIELD, originalMessage: {}, message: [{html: 'updated the Approval Mode from "Submit and Approve" to "Submit and Close"', type: 'Action type', text: 'Action text'}], @@ -654,6 +659,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-08 22:27:06.825', reportActionID: '1661970171066216', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.REIMBURSEMENT_QUEUED, originalMessage: { paymentType: 'ACH', @@ -663,6 +669,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-06 22:27:08.825', reportActionID: '1661970171066220', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.TASK_EDITED, originalMessage: { html: 'Hello world', @@ -684,6 +691,7 @@ describe('ReportActionsUtils', () => { const movedTransactionAction: ReportAction = { created: '2022-11-13 22:27:01.825', reportActionID: '8401445780099177', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.MOVED_TRANSACTION, originalMessage: { fromReportID: CONST.REPORT.UNREPORTED_REPORT_ID, @@ -694,6 +702,7 @@ describe('ReportActionsUtils', () => { const addCommentAction: ReportAction = { created: '2022-11-12 22:27:01.825', reportActionID: '6401435781022176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', @@ -719,6 +728,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-13 22:27:01.825', reportActionID: '8401445780099176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', @@ -735,6 +745,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-12 22:27:01.825', reportActionID: '6401435781022176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.CREATED, originalMessage: { html: 'Hello world', @@ -751,6 +762,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-11 22:27:01.825', reportActionID: '2962390724708756', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.IOU, originalMessage: { amount: 0, @@ -768,6 +780,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-10 22:27:01.825', reportActionID: '1609646094152486', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.RENAMED, originalMessage: { html: 'Hello world', @@ -786,6 +799,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-09 22:27:01.825', reportActionID: '1661970171066218', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.CLOSED, originalMessage: { policyName: 'default', // change to const @@ -814,6 +828,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-13 22:27:01.825', reportActionID: '8401445780099176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', @@ -830,6 +845,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-12 22:27:01.825', reportActionID: '8401445780099175', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', @@ -841,6 +857,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-11 22:27:01.825', reportActionID: '8401445780099174', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: 'Hello world', @@ -863,6 +880,7 @@ describe('ReportActionsUtils', () => { { created: '2024-11-19 08:04:13.728', reportActionID: '1607371725956675966', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: '', @@ -882,6 +900,7 @@ describe('ReportActionsUtils', () => { { created: '2024-11-19 08:00:14.352', reportActionID: '4655978522337302598', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT, originalMessage: { html: '#join', @@ -900,6 +919,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-09 22:27:01.825', reportActionID: '8049485084562457', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_REPORT_MENTION_WHISPER, originalMessage: { lastModified: '2024-11-19 08:00:14.353', @@ -917,6 +937,7 @@ describe('ReportActionsUtils', () => { { created: '2022-11-12 22:27:01.825', reportActionID: '6401435781022176', + reportID: '1', actionName: CONST.REPORT.ACTIONS.TYPE.ACTIONABLE_MENTION_WHISPER, originalMessage: { inviteeAccountIDs: [18414674], @@ -1053,6 +1074,7 @@ describe('ReportActionsUtils', () => { ...LHNTestUtils.getFakeReportAction('email1@test.com', 3), created: '2023-08-01 16:00:00', reportActionID: 'action1', + reportID: '1', actionName: 'ADDCOMMENT', originalMessage: { html: 'Hello world', @@ -1063,6 +1085,7 @@ describe('ReportActionsUtils', () => { ...LHNTestUtils.getFakeReportAction('email2@test.com', 3), created: '2023-08-01 18:00:00', reportActionID: 'action2', + reportID: '1', actionName: 'ADDCOMMENT', originalMessage: { html: 'Hello world', diff --git a/tests/unit/SidebarUtilsTest.ts b/tests/unit/SidebarUtilsTest.ts index 3a7e7eb1a984..47e4c6d0aba3 100644 --- a/tests/unit/SidebarUtilsTest.ts +++ b/tests/unit/SidebarUtilsTest.ts @@ -1816,6 +1816,7 @@ describe('SidebarUtils', () => { const lastAction: ReportAction = { ...createRandomReportAction(1), + reportID: iouReportR14932.reportID, message: [ { type: 'COMMENT', @@ -1839,8 +1840,16 @@ describe('SidebarUtils', () => { await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${iouReportR14932.reportID}`, iouReportR14932); await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${chatReportR14932.reportID}`, chatReportR14932); await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT}${report.reportID}`, report); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${report.reportID}`, {[lastAction.reportActionID]: lastAction}); - await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportR14932.reportID}`, {[linkedCreateAction.reportActionID]: linkedCreateAction}); + await Onyx.set(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${iouReportR14932.reportID}`, { + [linkedCreateAction.reportActionID]: linkedCreateAction, + [lastAction.reportActionID]: lastAction, + }); + await Onyx.set(ONYXKEYS.DERIVED.VISIBLE_REPORT_ACTIONS, { + [iouReportR14932.reportID]: { + [linkedCreateAction.reportActionID]: true, + [lastAction.reportActionID]: true, + }, + }); }); const result = SidebarUtils.getOptionData({ @@ -1858,6 +1867,12 @@ describe('SidebarUtils', () => { lastActionReport: undefined, isReportArchived: undefined, currentUserAccountID: session.accountID, + visibleReportActionsData: { + [iouReportR14932.reportID]: { + [linkedCreateAction.reportActionID]: true, + [lastAction.reportActionID]: true, + }, + }, }); expect(result?.alternateText).toBe(`You: ${getReportActionMessageText(lastAction)}`); @@ -1878,6 +1893,7 @@ describe('SidebarUtils', () => { }; const lastAction: ReportAction = { ...createRandomReportAction(1), + reportID: '1', message: [ { type: 'COMMENT', @@ -1897,6 +1913,7 @@ describe('SidebarUtils', () => { }; const deletedAction: ReportAction = { ...createRandomReportAction(2), + reportID: '1', actionName: 'IOU', actorAccountID: 20337430, automatic: false, @@ -1976,6 +1993,7 @@ describe('SidebarUtils', () => { lastAction, lastActionReport: undefined, isReportArchived: undefined, + lastMessageTextFromReport: 'test action', currentUserAccountID: 0, });