Skip to content

Commit 7e6f012

Browse files
authored
Merge pull request Expensify#89087 from callstack-internal/perf/decompose-optionrowlhn-avatar
perf: decompose OptionRowLHN avatar/delegate logic into a separate component
2 parents fea979d + e4b0a0f commit 7e6f012

2 files changed

Lines changed: 89 additions & 59 deletions

File tree

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
import React from 'react';
2+
import type {ColorValue, ViewStyle} from 'react-native';
3+
import type {OnyxEntry} from 'react-native-onyx';
4+
import {usePersonalDetails} from '@components/OnyxListItemProvider';
5+
import {shouldOptionShowTooltip} from '@libs/OptionsListUtils';
6+
import {getDelegateAccountIDFromReportAction} from '@libs/ReportActionsUtils';
7+
import type {OptionData} from '@libs/ReportUtils';
8+
import CONST from '@src/CONST';
9+
import type {Report} from '@src/types/onyx';
10+
import LHNAvatar from './LHNAvatar';
11+
12+
type OptionRowAvatarProps = {
13+
optionItem: OptionData;
14+
report: OnyxEntry<Report>;
15+
isInFocusMode: boolean;
16+
subscriptAvatarBorderColor: ColorValue;
17+
secondaryAvatarBackgroundColor: ColorValue;
18+
singleAvatarContainerStyle: ViewStyle[];
19+
};
20+
21+
function OptionRowAvatar({optionItem, report, isInFocusMode, subscriptAvatarBorderColor, secondaryAvatarBackgroundColor, singleAvatarContainerStyle}: OptionRowAvatarProps) {
22+
const personalDetails = usePersonalDetails();
23+
24+
const delegateAccountID = getDelegateAccountIDFromReportAction(optionItem?.parentReportAction);
25+
26+
// Match the header's delegate avatar logic: when a delegate exists on the
27+
// parent report action, the header (useReportActionAvatars) shows the
28+
// delegate's avatar as primary instead of the report owner's.
29+
const skipDelegate = report?.type === CONST.REPORT.TYPE.INVOICE || (optionItem?.isTaskReport && !report?.chatReportID);
30+
31+
let icons = optionItem?.icons ?? [];
32+
if (!skipDelegate && delegateAccountID && personalDetails && icons.length > 0) {
33+
const delegateDetails = personalDetails[delegateAccountID];
34+
if (delegateDetails) {
35+
const updatedIcons = [...icons];
36+
const firstDelegateIcon = updatedIcons.at(0);
37+
if (firstDelegateIcon) {
38+
updatedIcons[0] = {
39+
...firstDelegateIcon,
40+
source: delegateDetails.avatar ?? '',
41+
name: delegateDetails.displayName ?? '',
42+
id: delegateAccountID,
43+
};
44+
}
45+
icons = updatedIcons;
46+
}
47+
}
48+
49+
let delegateTooltipAccountID: number | undefined;
50+
if (!skipDelegate && delegateAccountID && personalDetails?.[delegateAccountID] && optionItem?.icons?.length) {
51+
delegateTooltipAccountID = Number(optionItem.icons.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID);
52+
}
53+
54+
const firstIcon = optionItem.icons?.at(0);
55+
56+
if (!optionItem.icons?.length || !firstIcon) {
57+
return null;
58+
}
59+
60+
return (
61+
<LHNAvatar
62+
icons={icons}
63+
shouldShowSubscript={!!optionItem.shouldShowSubscript}
64+
size={isInFocusMode ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT}
65+
subscriptAvatarBorderColor={subscriptAvatarBorderColor}
66+
useMidSubscriptSize={isInFocusMode}
67+
secondaryAvatarBackgroundColor={secondaryAvatarBackgroundColor}
68+
singleAvatarContainerStyle={singleAvatarContainerStyle}
69+
shouldShowTooltip={shouldOptionShowTooltip(optionItem)}
70+
delegateAccountID={skipDelegate ? undefined : delegateAccountID}
71+
delegateTooltipAccountID={delegateTooltipAccountID}
72+
/>
73+
);
74+
}
75+
76+
OptionRowAvatar.displayName = 'OptionRowAvatar';
77+
78+
export default OptionRowAvatar;

src/components/LHNOptionsList/OptionRowLHN.tsx

Lines changed: 11 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import Badge from '@components/Badge';
55
import DisplayNames from '@components/DisplayNames';
66
import Icon from '@components/Icon';
77
import OfflineWithFeedback from '@components/OfflineWithFeedback';
8-
import {usePersonalDetails, useSession} from '@components/OnyxListItemProvider';
8+
import {useSession} from '@components/OnyxListItemProvider';
99
import Text from '@components/Text';
1010
import Tooltip from '@components/Tooltip';
1111
import getContextMenuAccessibilityHint from '@components/utils/getContextMenuAccessibilityHint';
@@ -20,18 +20,17 @@ import useThemeStyles from '@hooks/useThemeStyles';
2020
import DateUtils from '@libs/DateUtils';
2121
import {containsCustomEmoji as containsCustomEmojiUtils, containsOnlyCustomEmoji} from '@libs/EmojiUtils';
2222
import FS from '@libs/Fullstory';
23-
import {shouldOptionShowTooltip, shouldUseBoldText} from '@libs/OptionsListUtils';
23+
import {shouldUseBoldText} from '@libs/OptionsListUtils';
2424
import ReportActionComposeFocusManager from '@libs/ReportActionComposeFocusManager';
25-
import {getDelegateAccountIDFromReportAction} from '@libs/ReportActionsUtils';
2625
import {isAdminRoom, isChatUsedForOnboarding as isChatUsedForOnboardingReportUtils, isConciergeChatReport, isGroupChat, isOneOnOneChat, isSystemChat} from '@libs/ReportUtils';
2726
import {startSpan} from '@libs/telemetry/activeSpans';
2827
import TextWithEmojiFragment from '@pages/inbox/report/comment/TextWithEmojiFragment';
2928
import FreeTrial from '@pages/settings/Subscription/FreeTrial';
3029
import variables from '@styles/variables';
3130
import CONST from '@src/CONST';
3231
import {isEmptyObject} from '@src/types/utils/EmptyObject';
33-
import LHNAvatar from './LHNAvatar';
3432
import {useLHNTooltipContext} from './LHNTooltipContext';
33+
import OptionRowAvatar from './OptionRowAvatar';
3534
import OptionRowPressable from './OptionRowPressable';
3635
import OptionRowTooltipLayer from './OptionRowTooltipLayer';
3736
import type {OptionRowLHNProps} from './types';
@@ -59,7 +58,6 @@ function OptionRowLHN({
5958
const {onboardingPurpose, onboarding, firstReportIDWithGBRorRBR, isScreenFocused} = useLHNTooltipContext();
6059
const shouldShowRBRorGBRTooltip = firstReportIDWithGBRorRBR === reportID;
6160

62-
const personalDetails = usePersonalDetails();
6361
const session = useSession();
6462
const isOnboardingGuideAssigned = onboardingPurpose === CONST.ONBOARDING_CHOICES.MANAGE_TEAM && !session?.email?.includes('+');
6563
const isChatUsedForOnboarding = isChatUsedForOnboardingReportUtils(report, onboarding, conciergeReportID, onboardingPurpose);
@@ -83,45 +81,6 @@ function OptionRowLHN({
8381
[optionItem?.alternateText],
8482
);
8583

86-
const delegateAccountID = useMemo(
87-
() => getDelegateAccountIDFromReportAction(optionItem?.parentReportAction),
88-
89-
[optionItem?.parentReportAction],
90-
);
91-
92-
// Match the header's delegate avatar logic: when a delegate exists on the
93-
// parent report action, the header (useReportActionAvatars) shows the
94-
// delegate's avatar as primary instead of the report owner's.
95-
const skipDelegate = report?.type === CONST.REPORT.TYPE.INVOICE || (optionItem?.isTaskReport && !report?.chatReportID);
96-
const icons = useMemo(() => {
97-
let result = optionItem?.icons ?? [];
98-
if (!skipDelegate && delegateAccountID && personalDetails && result.length > 0) {
99-
const delegateDetails = personalDetails[delegateAccountID];
100-
if (delegateDetails) {
101-
const updatedIcons = [...result];
102-
const firstIcon = updatedIcons.at(0);
103-
if (firstIcon) {
104-
updatedIcons[0] = {
105-
...firstIcon,
106-
source: delegateDetails.avatar ?? '',
107-
name: delegateDetails.displayName ?? '',
108-
id: delegateAccountID,
109-
};
110-
}
111-
result = updatedIcons;
112-
}
113-
}
114-
115-
return result;
116-
}, [optionItem?.icons, skipDelegate, delegateAccountID, personalDetails]);
117-
118-
const delegateTooltipAccountID = useMemo(() => {
119-
if (!skipDelegate && delegateAccountID && personalDetails?.[delegateAccountID] && optionItem?.icons?.length) {
120-
return Number(optionItem.icons.at(0)?.id ?? CONST.DEFAULT_NUMBER_ID);
121-
}
122-
return undefined;
123-
}, [skipDelegate, delegateAccountID, personalDetails, optionItem?.icons]);
124-
12584
const singleAvatarContainerStyle = [styles.actionAvatar, styles.mr3];
12685

12786
if (!optionItem && !isOptionFocused) {
@@ -170,7 +129,6 @@ function OptionRowLHN({
170129
const isStatusVisible = !!emojiCode && isOneOnOneChat(!isEmptyObject(report) ? report : undefined);
171130

172131
const subscriptAvatarBorderColor = isOptionFocused ? focusedBackgroundColor : theme.sidebar;
173-
const firstIcon = optionItem.icons?.at(0);
174132

175133
// This is used to ensure that we display the text exactly as the user entered it when displaying LHN title, instead of parsing their text to HTML.
176134
const shouldParseFullTitle = optionItem?.parentReportAction?.actionName !== CONST.REPORT.ACTIONS.TYPE.ADD_COMMENT && !isGroupChat(report);
@@ -227,20 +185,14 @@ function OptionRowLHN({
227185
<>
228186
<View style={sidebarInnerRowStyle}>
229187
<View style={[styles.flexRow, styles.alignItemsCenter]}>
230-
{!!optionItem.icons?.length && !!firstIcon && (
231-
<LHNAvatar
232-
icons={icons}
233-
shouldShowSubscript={!!optionItem.shouldShowSubscript}
234-
size={isInFocusMode ? CONST.AVATAR_SIZE.SMALL : CONST.AVATAR_SIZE.DEFAULT}
235-
subscriptAvatarBorderColor={hovered && !isOptionFocused ? hoveredBackgroundColor : subscriptAvatarBorderColor}
236-
useMidSubscriptSize={isInFocusMode}
237-
secondaryAvatarBackgroundColor={secondaryAvatarBgColor}
238-
singleAvatarContainerStyle={singleAvatarContainerStyle}
239-
shouldShowTooltip={shouldOptionShowTooltip(optionItem)}
240-
delegateAccountID={skipDelegate ? undefined : delegateAccountID}
241-
delegateTooltipAccountID={delegateTooltipAccountID}
242-
/>
243-
)}
188+
<OptionRowAvatar
189+
optionItem={optionItem}
190+
report={report}
191+
isInFocusMode={isInFocusMode}
192+
subscriptAvatarBorderColor={hovered && !isOptionFocused ? hoveredBackgroundColor : subscriptAvatarBorderColor}
193+
secondaryAvatarBackgroundColor={secondaryAvatarBgColor}
194+
singleAvatarContainerStyle={singleAvatarContainerStyle}
195+
/>
244196
<View style={contentContainerStyles}>
245197
<View style={[styles.flexRow, styles.alignItemsCenter, styles.mw100, styles.overflowHidden]}>
246198
<DisplayNames

0 commit comments

Comments
 (0)