Skip to content

Commit df70ec7

Browse files
authored
Merge pull request Expensify#89169 from callstack-internal/perf-extract-whisperbanner-searchactionheader
refactor: PureReportActionItem, extract WhisperBanner and SearchActionHeader
2 parents 1881b7b + cf4c7ff commit df70ec7

4 files changed

Lines changed: 152 additions & 81 deletions

File tree

src/pages/inbox/report/PureReportActionItem.tsx

Lines changed: 17 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,7 @@ import {Keyboard, View} from 'react-native';
88
import type {OnyxEntry} from 'react-native-onyx';
99
import type {ValueOf} from 'type-fest';
1010
import * as ActionSheetAwareScrollView from '@components/ActionSheetAwareScrollView';
11-
import DisplayNames from '@components/DisplayNames';
1211
import Hoverable from '@components/Hoverable';
13-
import Icon from '@components/Icon';
1412
import InlineSystemMessage from '@components/InlineSystemMessage';
1513
import {ModalActions} from '@components/Modal/Global/ModalContext';
1614
import OfflineWithFeedback from '@components/OfflineWithFeedback';
@@ -32,11 +30,8 @@ import UnreportedTransactionAction from '@components/ReportActionItem/Unreported
3230
import {SearchStateContext} from '@components/Search/SearchContext';
3331
import {useIsOnSearch} from '@components/Search/SearchScopeProvider';
3432
import {ShowContextMenuActionsContext, ShowContextMenuStateContext} from '@components/ShowContextMenuContext';
35-
import Text from '@components/Text';
36-
import TextLink from '@components/TextLink';
3733
import UnreadActionIndicator from '@components/UnreadActionIndicator';
3834
import useConfirmModal from '@hooks/useConfirmModal';
39-
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
4035
import useLocalize from '@hooks/useLocalize';
4136
import useOnyx from '@hooks/useOnyx';
4237
import usePrevious from '@hooks/usePrevious';
@@ -99,10 +94,7 @@ import {
9994
} from '@libs/ReportActionsUtils';
10095
import {
10196
canWriteInReport,
102-
getChatListItemReportName,
103-
getDisplayNamesWithTooltips,
10497
getMovedActionMessage,
105-
getWhisperDisplayNames,
10698
isCompletedTaskReport,
10799
isExpenseReport,
108100
isHarvestCreatedExpenseReport as isHarvestCreatedExpenseReportUtils,
@@ -112,7 +104,6 @@ import {
112104
import SelectionScraper from '@libs/SelectionScraper';
113105
import {ReactionListContext} from '@pages/inbox/ReportScreenContext';
114106
import AttachmentModalContext from '@pages/media/AttachmentModalScreen/AttachmentModalContext';
115-
import variables from '@styles/variables';
116107
import type {IgnoreDirection} from '@userActions/ClearReportActionErrors';
117108
import {hideEmojiPicker, isActive} from '@userActions/EmojiPickerAction';
118109
import {expandURLPreview} from '@userActions/Report';
@@ -149,7 +140,9 @@ import ReportActionItemDraft from './ReportActionItemDraft';
149140
import ReportActionItemGrouped from './ReportActionItemGrouped';
150141
import ReportActionItemSingle from './ReportActionItemSingle';
151142
import ReportActionItemThread from './ReportActionItemThread';
143+
import SearchActionHeader from './SearchActionHeader';
152144
import TripSummary from './TripSummary';
145+
import WhisperBanner from './WhisperBanner';
153146

154147
type PureReportActionItemProps = {
155148
/** The personal policy ID */
@@ -259,9 +252,6 @@ type PureReportActionItemProps = {
259252
/** Gets all transactions on an IOU report with a receipt */
260253
getTransactionsWithReceipts?: (iouReportID: string | undefined) => OnyxTypes.Transaction[];
261254

262-
/** Whether the current user is the only participant in the report */
263-
isCurrentUserTheOnlyParticipant?: (participantAccountIDs?: number[]) => boolean;
264-
265255
/** Function to clear an error from a transaction */
266256
clearError?: (transactionID: string) => void;
267257

@@ -333,7 +323,6 @@ function PureReportActionItem({
333323
resolveActionableReportMentionWhisper = () => {},
334324
resolveActionableMentionWhisper = () => {},
335325
isClosedExpenseReportWithNoExpenses,
336-
isCurrentUserTheOnlyParticipant = () => false,
337326
getTransactionsWithReceipts = () => [],
338327
clearError = () => {},
339328
clearAllRelatedReportActionErrors = () => {},
@@ -348,9 +337,8 @@ function PureReportActionItem({
348337
const isConciergeGreeting = action.reportActionID === CONST.CONCIERGE_GREETING_ACTION_ID;
349338
const shouldDisplayContextMenuValue = shouldDisplayContextMenu && !isConciergeGreeting;
350339

351-
const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
352340
const {transitionActionSheetState} = ActionSheetAwareScrollView.useActionSheetAwareScrollViewActions();
353-
const {translate, formatPhoneNumber, localeCompare, formatTravelDate, datetimeToCalendarTime} = useLocalize();
341+
const {translate, formatTravelDate, datetimeToCalendarTime} = useLocalize();
354342
const {showConfirmModal} = useConfirmModal();
355343
const {shouldUseNarrowLayout} = useResponsiveLayout();
356344
const reportID = report?.reportID ?? action?.reportID;
@@ -374,7 +362,6 @@ function PureReportActionItem({
374362
const isReportArchived = useReportIsArchived(reportID);
375363

376364
const isHarvestCreatedExpenseReport = isHarvestCreatedExpenseReportUtils(reportNameValuePairsOrigin, reportNameValuePairsOriginalID);
377-
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Eye']);
378365

379366
const [childReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(action.childReportID)}`);
380367
const [chatReport] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT}${getNonEmptyStringOnyxID(report?.chatReportID)}`);
@@ -1124,42 +1111,10 @@ function PureReportActionItem({
11241111

11251112
const hasErrors = !isEmptyValueObject(action.errors);
11261113
const whisperedTo = getWhisperedTo(action);
1127-
const isMultipleParticipant = whisperedTo.length > 1;
11281114

11291115
const iouReportID = isMoneyRequestAction(action) && getOriginalMessage(action)?.IOUReportID ? getOriginalMessage(action)?.IOUReportID?.toString() : undefined;
11301116
const transactionsWithReceipts = getTransactionsWithReceipts(iouReportID);
11311117
const isWhisper = whisperedTo.length > 0 && transactionsWithReceipts.length === 0;
1132-
const whisperedToPersonalDetails = isWhisper
1133-
? (Object.values(personalDetails ?? {}).filter((details) => whisperedTo.includes(details?.accountID ?? CONST.DEFAULT_NUMBER_ID)) as OnyxTypes.PersonalDetails[])
1134-
: [];
1135-
const isWhisperOnlyVisibleByUser = isWhisper && isCurrentUserTheOnlyParticipant(whisperedTo);
1136-
const displayNamesWithTooltips = isWhisper ? getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant, localeCompare, formatPhoneNumber) : [];
1137-
1138-
const renderSearchHeader = (children: React.ReactNode) => {
1139-
if (!isOnSearch) {
1140-
return children;
1141-
}
1142-
1143-
return (
1144-
<View style={[styles.p4]}>
1145-
<View style={styles.webViewStyles.tagStyles.ol}>
1146-
<View style={[styles.flexRow, styles.alignItemsCenter, !isWhisper ? styles.mb3 : {}]}>
1147-
<Text style={styles.chatItemMessageHeaderPolicy}>{translate('common.in')}&nbsp;</Text>
1148-
<TextLink
1149-
fontSize={variables.fontSizeSmall}
1150-
onPress={() => {
1151-
onPress?.();
1152-
}}
1153-
numberOfLines={1}
1154-
>
1155-
{getChatListItemReportName(action, report, conciergeReportID)}
1156-
</TextLink>
1157-
</View>
1158-
{children}
1159-
</View>
1160-
</View>
1161-
);
1162-
};
11631118

11641119
// Calculating accessibilityLabel for chat message with sender, date and time and the message content.
11651120
const displayName = getDisplayNameOrDefault(personalDetails?.[action.actorAccountID ?? CONST.DEFAULT_NUMBER_ID]);
@@ -1241,42 +1196,25 @@ function PureReportActionItem({
12411196
needsOffscreenAlphaCompositing={isMoneyRequestAction(action)}
12421197
shouldDisableStrikeThrough
12431198
>
1244-
{renderSearchHeader(
1245-
<>
1246-
{isWhisper && (
1247-
<View style={[styles.flexRow, styles.pl5, styles.pt2, styles.pr3]}>
1248-
<View style={[styles.pl6, styles.mr3]}>
1249-
<Icon
1250-
fill={theme.icon}
1251-
src={expensifyIcons.Eye}
1252-
small
1253-
/>
1254-
</View>
1255-
<Text style={[styles.chatItemMessageHeaderTimestamp]}>
1256-
{translate('reportActionContextMenu.onlyVisible')}
1257-
&nbsp;
1258-
</Text>
1259-
<DisplayNames
1260-
fullTitle={getWhisperDisplayNames(translate, formatPhoneNumber, whisperedTo) ?? ''}
1261-
displayNamesWithTooltips={displayNamesWithTooltips}
1262-
tooltipEnabled
1263-
numberOfLines={1}
1264-
textStyles={[styles.chatItemMessageHeaderTimestamp, styles.flex1]}
1265-
shouldUseFullTitle={isWhisperOnlyVisibleByUser}
1266-
/>
1267-
</View>
1268-
)}
1269-
{renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)}
1270-
</>,
1271-
)}
1199+
<SearchActionHeader
1200+
action={action}
1201+
report={report}
1202+
isWhisper={isWhisper}
1203+
onPress={onPress}
1204+
>
1205+
{isWhisper && <WhisperBanner whisperedTo={whisperedTo} />}
1206+
{renderReportActionItem(!!hovered || !!isReportActionLinked, isWhisper, hasErrors)}
1207+
</SearchActionHeader>
12721208
</OfflineWithFeedback>
12731209
</View>
12741210
</View>
12751211
)}
12761212
</Hoverable>
1277-
<View style={styles.reportActionSystemMessageContainer}>
1278-
<InlineSystemMessage message={action.error} />
1279-
</View>
1213+
{!!action.error && (
1214+
<View style={styles.reportActionSystemMessageContainer}>
1215+
<InlineSystemMessage message={action.error} />
1216+
</View>
1217+
)}
12801218
</PressableWithSecondaryInteraction>
12811219
</View>
12821220
);

src/pages/inbox/report/ReportActionItem.tsx

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import useReportIsArchived from '@hooks/useReportIsArchived';
66
import useReportTransactions from '@hooks/useReportTransactions';
77
import getNonEmptyStringOnyxID from '@libs/getNonEmptyStringOnyxID';
88
import {getIOUReportIDFromReportActionPreview, getOriginalMessage, isMoneyRequestAction} from '@libs/ReportActionsUtils';
9-
import {chatIncludesChronosWithID, getTransactionsWithReceipts, isArchivedNonExpenseReport, isClosedExpenseReportWithNoExpenses, isCurrentUserTheOnlyParticipant} from '@libs/ReportUtils';
9+
import {chatIncludesChronosWithID, getTransactionsWithReceipts, isArchivedNonExpenseReport, isClosedExpenseReportWithNoExpenses} from '@libs/ReportUtils';
1010
import {clearAllRelatedReportActionErrors} from '@userActions/ClearReportActionErrors';
1111
import {deleteReportActionDraft, resolveActionableMentionWhisper, resolveActionableReportMentionWhisper} from '@userActions/Report';
1212
import {clearError} from '@userActions/Transaction';
@@ -85,7 +85,6 @@ function ReportActionItem({
8585
resolveActionableReportMentionWhisper={resolveActionableReportMentionWhisper}
8686
resolveActionableMentionWhisper={resolveActionableMentionWhisper}
8787
isClosedExpenseReportWithNoExpenses={isClosedExpenseReportWithNoExpenses(iouReport, transactionsOnIOUReport)}
88-
isCurrentUserTheOnlyParticipant={isCurrentUserTheOnlyParticipant}
8988
getTransactionsWithReceipts={getTransactionsWithReceipts}
9089
clearError={clearError}
9190
clearAllRelatedReportActionErrors={clearAllRelatedReportActionErrors}
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
import React from 'react';
2+
import {View} from 'react-native';
3+
import type {OnyxEntry} from 'react-native-onyx';
4+
import {useIsOnSearch} from '@components/Search/SearchScopeProvider';
5+
import Text from '@components/Text';
6+
import TextLink from '@components/TextLink';
7+
import useLocalize from '@hooks/useLocalize';
8+
import useOnyx from '@hooks/useOnyx';
9+
import useThemeStyles from '@hooks/useThemeStyles';
10+
import {getChatListItemReportName} from '@libs/ReportUtils';
11+
import variables from '@styles/variables';
12+
import ONYXKEYS from '@src/ONYXKEYS';
13+
import type {Report, ReportAction} from '@src/types/onyx';
14+
15+
type SearchActionHeaderProps = {
16+
/** The report action being rendered. */
17+
action: ReportAction;
18+
/** The report this action belongs to. */
19+
report: OnyxEntry<Report>;
20+
/** Whether the underlying action is a whisper. Used to skip bottom margin when WhisperBanner is rendered below. */
21+
isWhisper: boolean;
22+
/** Tap handler for the report-name link. */
23+
onPress?: () => void;
24+
/** The action content to render below the header. */
25+
children: React.ReactNode;
26+
};
27+
28+
function SearchActionHeaderContent({action, report, isWhisper, onPress, children}: SearchActionHeaderProps) {
29+
const styles = useThemeStyles();
30+
const {translate} = useLocalize();
31+
const [conciergeReportID] = useOnyx(ONYXKEYS.CONCIERGE_REPORT_ID);
32+
33+
const reportName = getChatListItemReportName(action, report, conciergeReportID);
34+
35+
return (
36+
<View style={[styles.p4]}>
37+
<View style={styles.webViewStyles.tagStyles.ol}>
38+
<View style={[styles.flexRow, styles.alignItemsCenter, !isWhisper ? styles.mb3 : {}]}>
39+
<Text style={styles.chatItemMessageHeaderPolicy}>{translate('common.in')}&nbsp;</Text>
40+
<TextLink
41+
fontSize={variables.fontSizeSmall}
42+
onPress={() => {
43+
onPress?.();
44+
}}
45+
numberOfLines={1}
46+
>
47+
{reportName}
48+
</TextLink>
49+
</View>
50+
{children}
51+
</View>
52+
</View>
53+
);
54+
}
55+
56+
function SearchActionHeader({action, report, isWhisper, onPress, children}: SearchActionHeaderProps) {
57+
const isOnSearch = useIsOnSearch();
58+
59+
if (!isOnSearch) {
60+
return children;
61+
}
62+
63+
return (
64+
<SearchActionHeaderContent
65+
action={action}
66+
report={report}
67+
isWhisper={isWhisper}
68+
onPress={onPress}
69+
>
70+
{children}
71+
</SearchActionHeaderContent>
72+
);
73+
}
74+
75+
export default SearchActionHeader;
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import React from 'react';
2+
import {View} from 'react-native';
3+
import DisplayNames from '@components/DisplayNames';
4+
import Icon from '@components/Icon';
5+
import {usePersonalDetails} from '@components/OnyxListItemProvider';
6+
import Text from '@components/Text';
7+
import {useMemoizedLazyExpensifyIcons} from '@hooks/useLazyAsset';
8+
import useLocalize from '@hooks/useLocalize';
9+
import useTheme from '@hooks/useTheme';
10+
import useThemeStyles from '@hooks/useThemeStyles';
11+
import {getDisplayNamesWithTooltips, getWhisperDisplayNames, isCurrentUserTheOnlyParticipant} from '@libs/ReportUtils';
12+
import CONST from '@src/CONST';
13+
import type * as OnyxTypes from '@src/types/onyx';
14+
15+
type WhisperBannerProps = {
16+
/** Account IDs of the users that can see the whisper. */
17+
whisperedTo: number[];
18+
};
19+
20+
function WhisperBanner({whisperedTo}: WhisperBannerProps) {
21+
const styles = useThemeStyles();
22+
const theme = useTheme();
23+
const {translate, formatPhoneNumber, localeCompare} = useLocalize();
24+
const expensifyIcons = useMemoizedLazyExpensifyIcons(['Eye']);
25+
const personalDetails = usePersonalDetails();
26+
27+
const isMultipleParticipant = whisperedTo.length > 1;
28+
const isWhisperOnlyVisibleByUser = isCurrentUserTheOnlyParticipant(whisperedTo);
29+
const whisperedToPersonalDetails = Object.values(personalDetails ?? {}).filter((details) =>
30+
whisperedTo.includes(details?.accountID ?? CONST.DEFAULT_NUMBER_ID),
31+
) as OnyxTypes.PersonalDetails[];
32+
const displayNamesWithTooltips = getDisplayNamesWithTooltips(whisperedToPersonalDetails, isMultipleParticipant, localeCompare, formatPhoneNumber);
33+
34+
return (
35+
<View style={[styles.flexRow, styles.pl5, styles.pt2, styles.pr3]}>
36+
<View style={[styles.pl6, styles.mr3]}>
37+
<Icon
38+
fill={theme.icon}
39+
src={expensifyIcons.Eye}
40+
small
41+
/>
42+
</View>
43+
<Text style={[styles.chatItemMessageHeaderTimestamp]}>
44+
{translate('reportActionContextMenu.onlyVisible')}
45+
&nbsp;
46+
</Text>
47+
<DisplayNames
48+
fullTitle={getWhisperDisplayNames(translate, formatPhoneNumber, whisperedTo) ?? ''}
49+
displayNamesWithTooltips={displayNamesWithTooltips}
50+
tooltipEnabled
51+
numberOfLines={1}
52+
textStyles={[styles.chatItemMessageHeaderTimestamp, styles.flex1]}
53+
shouldUseFullTitle={isWhisperOnlyVisibleByUser}
54+
/>
55+
</View>
56+
);
57+
}
58+
59+
export default WhisperBanner;

0 commit comments

Comments
 (0)