Skip to content

Commit d2c0d7a

Browse files
authored
Merge pull request Expensify#89468 from callstack-internal/perf-purereportactionitem-cleanup
refactor: PureReportActionItem, shrink state and prop surface
2 parents 7b2887d + 8487737 commit d2c0d7a

15 files changed

Lines changed: 330 additions & 167 deletions

src/libs/ReportActionsUtils.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import type {
2929
VisibleReportActionsDerivedValue,
3030
} from '@src/types/onyx';
3131
import type {
32+
DecisionName,
3233
JoinWorkspaceResolution,
3334
OriginalMessageChangeLog,
3435
OriginalMessageExportIntegration,
@@ -259,6 +260,23 @@ function isPendingRemove(reportAction: OnyxInputOrEntry<ReportAction>): boolean
259260
return getReportActionMessage(reportAction)?.moderationDecision?.decision === CONST.MODERATION.MODERATOR_DECISION_PENDING_REMOVE;
260261
}
261262

263+
/**
264+
* Derives the moderation decision and whether the action is flagged from the action itself.
265+
* Used by leaves that previously received the equivalent values as props from PureReportActionItem.
266+
*/
267+
function getModerationFlagState(reportAction: OnyxInputOrEntry<ReportAction>): {latestDecision: DecisionName | undefined; hasBeenFlagged: boolean} {
268+
// Moderation only applies to ADD_COMMENT actions
269+
if (reportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT) {
270+
return {latestDecision: undefined, hasBeenFlagged: false};
271+
}
272+
const latestDecision = getReportActionMessage(reportAction)?.moderationDecision?.decision;
273+
const hasBeenFlagged =
274+
!!latestDecision &&
275+
![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === latestDecision) &&
276+
!isPendingRemove(reportAction);
277+
return {latestDecision, hasBeenFlagged};
278+
}
279+
262280
function isMoneyRequestAction(reportAction: OnyxInputOrEntry<ReportAction>): reportAction is ReportAction<typeof CONST.REPORT.ACTIONS.TYPE.IOU> {
263281
return isActionOfType(reportAction, CONST.REPORT.ACTIONS.TYPE.IOU);
264282
}
@@ -4634,6 +4652,7 @@ export {
46344652
isOldDotReportAction,
46354653
isPayAction,
46364654
isPendingRemove,
4655+
getModerationFlagState,
46374656
isPolicyChangeLogAction,
46384657
isReimbursementDeQueuedOrCanceledAction,
46394658
isReimbursementQueuedAction,
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import {personalDetailsSelector} from '@selectors/PersonalDetails';
2+
import React from 'react';
3+
import Text from '@components/Text';
4+
import useLocalize from '@hooks/useLocalize';
5+
import useOnyx from '@hooks/useOnyx';
6+
import usePersonalDetailsByLogin from '@hooks/usePersonalDetailsByLogin';
7+
import useThemeStyles from '@hooks/useThemeStyles';
8+
import CONST from '@src/CONST';
9+
import ONYXKEYS from '@src/ONYXKEYS';
10+
11+
type DelegateOnBehalfOfTextFallbackProps = {
12+
/** Fallback login looked up in the personal-details map when the account ID is not yet hydrated. */
13+
fallbackLogin: string | undefined;
14+
};
15+
16+
function DelegateOnBehalfOfTextFallback({fallbackLogin}: DelegateOnBehalfOfTextFallbackProps) {
17+
const styles = useThemeStyles();
18+
const {translate} = useLocalize();
19+
const personalDetailsByLogin = usePersonalDetailsByLogin();
20+
const detail = personalDetailsByLogin[fallbackLogin?.toLowerCase() ?? ''];
21+
return <Text style={[styles.chatDelegateMessage]}>{translate('delegate.onBehalfOfMessage', detail?.displayName ?? '')}</Text>;
22+
}
23+
24+
type DelegateOnBehalfOfTextProps = {
25+
/** The account ID whose login drives the "on behalf of" name. */
26+
mainAccountID: number | undefined;
27+
28+
/** Fallback login if the account is not yet present in personal details. */
29+
fallbackLogin: string | undefined;
30+
};
31+
32+
function DelegateOnBehalfOfText({mainAccountID, fallbackLogin}: DelegateOnBehalfOfTextProps) {
33+
const styles = useThemeStyles();
34+
const {translate} = useLocalize();
35+
const [resolvedDetail] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {selector: personalDetailsSelector(mainAccountID ?? CONST.DEFAULT_NUMBER_ID)});
36+
37+
if (!resolvedDetail?.login) {
38+
return <DelegateOnBehalfOfTextFallback fallbackLogin={fallbackLogin} />;
39+
}
40+
return <Text style={[styles.chatDelegateMessage]}>{translate('delegate.onBehalfOfMessage', resolvedDetail.displayName ?? '')}</Text>;
41+
}
42+
43+
export default DelegateOnBehalfOfText;
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import React from 'react';
2+
import type {OnyxEntry} from 'react-native-onyx';
3+
import Text from '@components/Text';
4+
import useLocalize from '@hooks/useLocalize';
5+
import useOnyx from '@hooks/useOnyx';
6+
import useThemeStyles from '@hooks/useThemeStyles';
7+
import {getHumanAgentFirstName} from '@libs/ReportActionsUtils';
8+
import ONYXKEYS from '@src/ONYXKEYS';
9+
import type * as OnyxTypes from '@src/types/onyx';
10+
11+
type HumanAgentAssistedByTextProps = {
12+
/** The action whose human agent's first name drives the "assisted by" label. */
13+
action: OnyxEntry<OnyxTypes.ReportAction>;
14+
};
15+
16+
function HumanAgentAssistedByText({action}: HumanAgentAssistedByTextProps) {
17+
const styles = useThemeStyles();
18+
const {translate} = useLocalize();
19+
const [humanAgentName] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
20+
selector: (list: OnyxEntry<OnyxTypes.PersonalDetailsList>) => getHumanAgentFirstName(action, list),
21+
});
22+
return <Text style={[styles.chatDelegateMessage]}>{translate('reportAction.assistedBy', humanAgentName ?? translate('reportAction.humanSupportAgent'))}</Text>;
23+
}
24+
25+
export default HumanAgentAssistedByText;

src/pages/inbox/report/PureReportActionItem.tsx

Lines changed: 6 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,10 @@
22
import {useNavigation} from '@react-navigation/native';
33
import {deepEqual} from 'fast-equals';
44
import mapValues from 'lodash/mapValues';
5-
import React, {memo, use, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
5+
import React, {memo, useCallback, useContext, useEffect, useMemo, useRef, useState} from 'react';
66
import type {GestureResponderEvent, TextInput} from 'react-native';
77
import {Keyboard, View} from 'react-native';
88
import type {OnyxEntry} from 'react-native-onyx';
9-
import type {ValueOf} from 'type-fest';
109
import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView';
1110
import Hoverable from '@components/Hoverable';
1211
import InlineSystemMessage from '@components/InlineSystemMessage';
@@ -27,7 +26,6 @@ import TaskAction from '@components/ReportActionItem/TaskAction';
2726
import TaskPreview from '@components/ReportActionItem/TaskPreview';
2827
import TripRoomPreview from '@components/ReportActionItem/TripRoomPreview';
2928
import UnreportedTransactionAction from '@components/ReportActionItem/UnreportedTransactionAction';
30-
import {SearchStateContext} from '@components/Search/SearchContext';
3129
import {useIsOnSearch} from '@components/Search/SearchScopeProvider';
3230
import {ShowContextMenuActionsContext, ShowContextMenuStateContext} from '@components/ShowContextMenuContext';
3331
import UnreadActionIndicator from '@components/UnreadActionIndicator';
@@ -94,6 +92,7 @@ import {
9492
import {
9593
canWriteInReport,
9694
getMovedActionMessage,
95+
getTransactionsWithReceipts,
9796
isCompletedTaskReport,
9897
isExpenseReport,
9998
isHarvestCreatedExpenseReport as isHarvestCreatedExpenseReportUtils,
@@ -103,9 +102,10 @@ import {
103102
import SelectionScraper from '@libs/SelectionScraper';
104103
import {ReactionListContext} from '@pages/inbox/ReportScreenContext';
105104
import AttachmentModalContext from '@pages/media/AttachmentModalScreen/AttachmentModalContext';
106-
import type {IgnoreDirection} from '@userActions/ClearReportActionErrors';
105+
import {clearAllRelatedReportActionErrors} from '@userActions/ClearReportActionErrors';
107106
import {hideEmojiPicker, isActive} from '@userActions/EmojiPickerAction';
108107
import {expandURLPreview} from '@userActions/Report';
108+
import {clearError} from '@userActions/Transaction';
109109
import CONST from '@src/CONST';
110110
import ONYXKEYS from '@src/ONYXKEYS';
111111
import type SCREENS from '@src/SCREENS';
@@ -219,41 +219,9 @@ type PureReportActionItemProps = {
219219
/** Whether the room is a chronos report */
220220
isChronosReport?: boolean;
221221

222-
/** Function to resolve actionable report mention whisper */
223-
resolveActionableReportMentionWhisper?: (
224-
report: OnyxEntry<OnyxTypes.Report>,
225-
reportAction: OnyxEntry<OnyxTypes.ReportAction>,
226-
resolution: ValueOf<typeof CONST.REPORT.ACTIONABLE_REPORT_MENTION_WHISPER_RESOLUTION>,
227-
isReportArchived?: boolean,
228-
) => void;
229-
230-
/** Function to resolve actionable mention whisper */
231-
resolveActionableMentionWhisper?: (
232-
report: OnyxEntry<OnyxTypes.Report>,
233-
reportAction: OnyxEntry<OnyxTypes.ReportAction>,
234-
resolution: ValueOf<typeof CONST.REPORT.ACTIONABLE_MENTION_WHISPER_RESOLUTION>,
235-
isReportArchived: boolean,
236-
parentReport?: OnyxEntry<OnyxTypes.Report>,
237-
) => void;
238-
239222
/** Whether the provided report is a closed expense report with no expenses */
240223
isClosedExpenseReportWithNoExpenses?: boolean;
241224

242-
/** Gets all transactions on an IOU report with a receipt */
243-
getTransactionsWithReceipts?: (iouReportID: string | undefined) => OnyxTypes.Transaction[];
244-
245-
/** Function to clear an error from a transaction */
246-
clearError?: (transactionID: string) => void;
247-
248-
/** Function to clear all errors from a report action */
249-
clearAllRelatedReportActionErrors?: (
250-
reportID: string | undefined,
251-
reportAction: OnyxTypes.ReportAction | null | undefined,
252-
originalReportID: string | undefined,
253-
ignore?: IgnoreDirection,
254-
keys?: string[],
255-
) => void;
256-
257225
/** User payment card ID */
258226
userBillingFundID?: number;
259227

@@ -304,12 +272,7 @@ function PureReportActionItem({
304272
deleteReportActionDraft = () => {},
305273
isArchivedRoom,
306274
isChronosReport,
307-
resolveActionableReportMentionWhisper = () => {},
308-
resolveActionableMentionWhisper = () => {},
309275
isClosedExpenseReportWithNoExpenses,
310-
getTransactionsWithReceipts = () => [],
311-
clearError = () => {},
312-
clearAllRelatedReportActionErrors = () => {},
313276
userBillingFundID,
314277
shouldShowBorder,
315278
shouldHighlight = false,
@@ -333,7 +296,6 @@ function PureReportActionItem({
333296
const [isPaymentMethodPopoverActive, setIsPaymentMethodPopoverActive] = useState<boolean | undefined>();
334297
const shouldRenderViewBasedOnAction = useTableReportViewActionRenderConditionals(action);
335298
const [isHidden, setIsHidden] = useState(false);
336-
const [moderationDecision, setModerationDecision] = useState<OnyxTypes.DecisionName>(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
337299
const {isActiveReportAction: isActiveReactionListReportAction, hideReactionList} = useContext(ReactionListContext);
338300
const {updateHiddenAttachments} = useContext(AttachmentModalContext);
339301
const composerTextInputRef = useRef<TextInput | HTMLTextAreaElement>(null);
@@ -372,11 +334,6 @@ function PureReportActionItem({
372334
);
373335

374336
const isOnSearch = useIsOnSearch();
375-
let currentSearchHash: number | undefined;
376-
if (isOnSearch) {
377-
const {currentSearchHash: searchContextCurrentSearchHash} = use(SearchStateContext);
378-
currentSearchHash = searchContextCurrentSearchHash;
379-
}
380337

381338
const navigation = useNavigation<PlatformStackNavigationProp<ReportsSplitNavigatorParamList, typeof SCREENS.REPORT>>();
382339
const dismissError = useCallback(() => {
@@ -392,7 +349,7 @@ function PureReportActionItem({
392349
clearError(transactionID);
393350
}
394351
clearAllRelatedReportActionErrors(reportID, action, originalReportID);
395-
}, [action, isSendingMoney, reportID, clearAllRelatedReportActionErrors, originalReportID, isReportActionLinked, report, chatReport, clearError, navigation]);
352+
}, [action, isSendingMoney, reportID, originalReportID, isReportActionLinked, report, chatReport, navigation]);
396353

397354
const showDismissReceiptErrorModal = useCallback(async () => {
398355
const result = await showConfirmModal({
@@ -487,12 +444,10 @@ function PureReportActionItem({
487444

488445
// Hide reveal message button and show the message if latestDecision is changed to empty
489446
if (!latestDecision) {
490-
setModerationDecision(CONST.MODERATION.MODERATOR_DECISION_APPROVED);
491447
setIsHidden(false);
492448
return;
493449
}
494450

495-
setModerationDecision(latestDecision);
496451
if (![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === latestDecision) && !isPendingRemove(action)) {
497452
setIsHidden(true);
498453
return;
@@ -841,7 +796,6 @@ function PureReportActionItem({
841796
report={report}
842797
originalReport={originalReport}
843798
originalReportID={originalReportID}
844-
resolveActionableMentionWhisper={resolveActionableMentionWhisper}
845799
/>
846800
);
847801
} else if (isActionableReportMentionWhisper(action)) {
@@ -852,7 +806,6 @@ function PureReportActionItem({
852806
report={report}
853807
originalReport={originalReport}
854808
isReportArchived={isReportArchived}
855-
resolveActionableReportMentionWhisper={resolveActionableReportMentionWhisper}
856809
/>
857810
);
858811
} else if (isActionableMentionInviteToSubmitExpenseConfirmWhisper(action)) {
@@ -931,12 +884,10 @@ function PureReportActionItem({
931884
draftMessage={draftMessage}
932885
index={index}
933886
isHidden={isHidden}
934-
moderationDecision={moderationDecision}
935887
updateHiddenState={updateHiddenState}
936888
isArchivedRoom={isArchivedRoom}
937889
composerTextInputRef={composerTextInputRef}
938890
isOnSearch={isOnSearch}
939-
currentSearchHash={currentSearchHash}
940891
contextMenuStateValue={contextMenuStateValue}
941892
contextMenuActionsValue={contextMenuActionsValue}
942893
userBillingFundID={userBillingFundID}
@@ -1024,9 +975,6 @@ function PureReportActionItem({
1024975
iouReport={iouReport}
1025976
isHovered={hovered || isContextMenuActive}
1026977
isActive={isReportActionActive && !isContextMenuActive}
1027-
hasBeenFlagged={
1028-
![CONST.MODERATION.MODERATOR_DECISION_APPROVED, CONST.MODERATION.MODERATOR_DECISION_PENDING].some((item) => item === moderationDecision) && !isPendingRemove(action)
1029-
}
1030978
>
1031979
{content}
1032980
</ReportActionItemSingle>
@@ -1089,8 +1037,7 @@ function PureReportActionItem({
10891037
const whisperedTo = getWhisperedTo(action);
10901038

10911039
const iouReportID = isMoneyRequestAction(action) && getOriginalMessage(action)?.IOUReportID ? getOriginalMessage(action)?.IOUReportID?.toString() : undefined;
1092-
const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID);
1093-
const isWhisper = whisperedTo.length > 0 && transactionsWithReceipts.length === 0;
1040+
const isWhisper = whisperedTo.length > 0 && getTransactionsWithReceipts(iouReportID).length === 0;
10941041

10951042
// Calculating accessibilityLabel for chat message with sender, date and time and the message content.
10961043
const displayName = getDisplayNameOrDefault(personalDetails?.[action.actorAccountID ?? CONST.DEFAULT_NUMBER_ID]);

src/pages/inbox/report/ReportActionItem.tsx

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,10 +5,8 @@ import useOriginalReportID from '@hooks/useOriginalReportID';
55
import useReportIsArchived from '@hooks/useReportIsArchived';
66
import useReportTransactions from '@hooks/useReportTransactions';
77
import {getIOUReportIDFromReportActionPreview, getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
8-
import {chatIncludesChronosWithID, getTransactionsWithReceipts, isArchivedNonExpenseReport, isClosedExpenseReportWithNoExpenses} from '@libs/ReportUtils';
9-
import {clearAllRelatedReportActionErrors} from '@userActions/ClearReportActionErrors';
10-
import {deleteReportActionDraft, resolveActionableMentionWhisper, resolveActionableReportMentionWhisper} from '@userActions/Report';
11-
import {clearError} from '@userActions/Transaction';
8+
import {chatIncludesChronosWithID, isArchivedNonExpenseReport, isClosedExpenseReportWithNoExpenses} from '@libs/ReportUtils';
9+
import {deleteReportActionDraft} from '@userActions/Report';
1210
import ONYXKEYS from '@src/ONYXKEYS';
1311
import type {PersonalDetailsList, Transaction} from '@src/types/onyx';
1412
import type {PureReportActionItemProps} from './PureReportActionItem';
@@ -74,12 +72,7 @@ function ReportActionItem({
7472
deleteReportActionDraft={deleteReportActionDraft}
7573
isArchivedRoom={isArchivedNonExpenseReport(originalReport, isOriginalReportArchived)}
7674
isChronosReport={chatIncludesChronosWithID(originalReportID)}
77-
resolveActionableReportMentionWhisper={resolveActionableReportMentionWhisper}
78-
resolveActionableMentionWhisper={resolveActionableMentionWhisper}
7975
isClosedExpenseReportWithNoExpenses={isClosedExpenseReportWithNoExpenses(iouReport, transactionsOnIOUReport)}
80-
getTransactionsWithReceipts={getTransactionsWithReceipts}
81-
clearError={clearError}
82-
clearAllRelatedReportActionErrors={clearAllRelatedReportActionErrors}
8376
userBillingFundID={userBillingFundID}
8477
isTryNewDotNVPDismissed={isTryNewDotNVPDismissed}
8578
/>

0 commit comments

Comments
 (0)