Skip to content

Commit 095540c

Browse files
authored
Merge pull request Expensify#65740 from software-mansion-labs/korytko/avatars-reports
[Avatars - Reports] Use centralized logic for report actions avatars everywhere in the app
2 parents 5a1bd16 + dc711d9 commit 095540c

60 files changed

Lines changed: 1847 additions & 1583 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

__mocks__/reportData/reports.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import CONST from '@src/CONST';
22
import type {Report} from '@src/types/onyx';
33

4-
const usersIDs = [15593135, 51760358, 26502375];
4+
const usersIDs = [15593135, 51760358, 26502375] as const;
55
const amount = 10402;
66
const currency = CONST.CURRENCY.USD;
77

@@ -20,7 +20,7 @@ const participants = usersIDs.reduce((prev, userID) => {
2020
};
2121
}, {});
2222

23-
const iouReportR14932: Report = {
23+
const iouReportR14932 = {
2424
currency,
2525
participants,
2626
total: amount,
@@ -31,9 +31,9 @@ const iouReportR14932: Report = {
3131
parentReportActionID: PARENT_REPORT_ACTION_ID_R14932,
3232
parentReportID: PARENT_REPORT_ID_R14932,
3333
reportID: REPORT_ID_R14932,
34-
lastActorAccountID: usersIDs.at(0),
35-
ownerAccountID: usersIDs.at(0),
36-
managerID: usersIDs.at(1),
34+
lastActorAccountID: usersIDs[0],
35+
ownerAccountID: usersIDs[0],
36+
managerID: usersIDs[1],
3737
permissions: [CONST.REPORT.PERMISSIONS.READ, CONST.REPORT.PERMISSIONS.WRITE],
3838
policyID: CONST.POLICY.ID_FAKE,
3939
reportName: CONST.REPORT.ACTIONS.TYPE.IOU,
@@ -60,7 +60,7 @@ const iouReportR14932: Report = {
6060
welcomeMessage: '',
6161
description: '',
6262
oldPolicyName: '',
63-
};
63+
} satisfies Report;
6464

6565
const chatReportR14932: Report = {
6666
currency,

src/CONST/index.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,21 @@ const CONST = {
292292
AVATAR_MIN_WIDTH_PX: 80,
293293
AVATAR_MIN_HEIGHT_PX: 80,
294294

295+
REPORT_ACTION_AVATARS: {
296+
TYPE: {
297+
MULTIPLE: 'multiple',
298+
MULTIPLE_DIAGONAL: 'multipleDiagonal',
299+
MULTIPLE_HORIZONTAL: 'multipleHorizontal',
300+
SUBSCRIPT: 'subscript',
301+
SINGLE: 'single',
302+
},
303+
SORT_BY: {
304+
ID: 'id',
305+
NAME: 'name',
306+
REVERSE: 'reverse',
307+
},
308+
},
309+
295310
// Maximum width and height size in px for a selected image
296311
AVATAR_MAX_WIDTH_PX: 4096,
297312
AVATAR_MAX_HEIGHT_PX: 4096,
@@ -3127,6 +3142,7 @@ const CONST = {
31273142
SMALL_SUBSCRIPT: 'small-subscript',
31283143
MID_SUBSCRIPT: 'mid-subscript',
31293144
LARGE_BORDERED: 'large-bordered',
3145+
MEDIUM_LARGE: 'medium-large',
31303146
HEADER: 'header',
31313147
MENTION_ICON: 'mention-icon',
31323148
SMALL_NORMAL: 'small-normal',

src/components/AnonymousReportFooter.tsx

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import React from 'react';
22
import {View} from 'react-native';
33
import type {OnyxEntry} from 'react-native-onyx';
44
import useLocalize from '@hooks/useLocalize';
5-
import usePolicy from '@hooks/usePolicy';
65
import useThemeStyles from '@hooks/useThemeStyles';
6+
// eslint-disable-next-line no-restricted-syntax
77
import {signOutAndRedirectToSignIn} from '@userActions/Session';
88
import type {Report} from '@src/types/onyx';
99
import AvatarWithDisplayName from './AvatarWithDisplayName';
@@ -23,16 +23,13 @@ function AnonymousReportFooter({isSmallSizeLayout = false, report}: AnonymousRep
2323
const styles = useThemeStyles();
2424
const {translate} = useLocalize();
2525

26-
const policy = usePolicy(report?.policyID);
27-
2826
return (
2927
<View style={styles.anonymousRoomFooter(isSmallSizeLayout)}>
3028
<View style={[styles.flexRow, styles.flexShrink1]}>
3129
<AvatarWithDisplayName
3230
report={report}
3331
isAnonymous
3432
shouldEnableDetailPageNavigation
35-
policy={policy}
3633
/>
3734
</View>
3835
<View style={styles.anonymousRoomFooterWordmarkAndLogoContainer(isSmallSizeLayout)}>

src/components/Avatar.tsx

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,9 @@ type AvatarProps = {
5353

5454
/** Optional account id if it's user avatar or policy id if it's workspace avatar */
5555
avatarID?: number | string;
56+
57+
/** Test ID for the Avatar component */
58+
testID?: string;
5659
};
5760

5861
function Avatar({
@@ -67,6 +70,7 @@ function Avatar({
6770
type,
6871
name = '',
6972
avatarID,
73+
testID = 'Avatar',
7074
}: AvatarProps) {
7175
const theme = useTheme();
7276
const styles = useThemeStyles();
@@ -103,7 +107,10 @@ function Avatar({
103107
}
104108

105109
return (
106-
<View style={[containerStyles, styles.pointerEventsNone]}>
110+
<View
111+
style={[containerStyles, styles.pointerEventsNone]}
112+
testID={testID}
113+
>
107114
{typeof avatarSource === 'string' ? (
108115
<View style={[iconStyle, StyleUtils.getAvatarBorderStyle(size, type), iconAdditionalStyles]}>
109116
<Image

src/components/AvatarWithDisplayName.tsx

Lines changed: 26 additions & 83 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import type {ColorValue, TextStyle} from 'react-native';
44
import type {OnyxEntry} from 'react-native-onyx';
55
import type {ValueOf} from 'type-fest';
66
import useOnyx from '@hooks/useOnyx';
7-
import type {ReportAvatarDetails} from '@hooks/useReportAvatarDetails';
8-
import useReportIsArchived from '@hooks/useReportIsArchived';
97
import useStyleUtils from '@hooks/useStyleUtils';
108
import useTheme from '@hooks/useTheme';
119
import useThemeStyles from '@hooks/useThemeStyles';
@@ -15,7 +13,6 @@ import type {DisplayNameWithTooltips} from '@libs/ReportUtils';
1513
import {
1614
getChatRoomSubtitle,
1715
getDisplayNamesWithTooltips,
18-
getIcons,
1916
getParentNavigationSubtitle,
2017
getReportName,
2118
isChatThread,
@@ -26,32 +23,24 @@ import {
2623
isMoneyRequestReport,
2724
isTrackExpenseReport,
2825
navigateToDetailsPage,
29-
shouldReportShowSubscript,
3026
} from '@libs/ReportUtils';
3127
import CONST from '@src/CONST';
3228
import ONYXKEYS from '@src/ONYXKEYS';
3329
import ROUTES from '@src/ROUTES';
34-
import type {Policy, Report} from '@src/types/onyx';
35-
import type {Icon} from '@src/types/onyx/OnyxCommon';
30+
import type {Report} from '@src/types/onyx';
3631
import {getButtonRole} from './Button/utils';
3732
import DisplayNames from './DisplayNames';
3833
import type DisplayNamesProps from './DisplayNames/types';
39-
import {FallbackAvatar} from './Icon/Expensicons';
40-
import MultipleAvatars from './MultipleAvatars';
4134
import ParentNavigationSubtitle from './ParentNavigationSubtitle';
4235
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
43-
import SingleReportAvatar from './ReportActionItem/SingleReportAvatar';
36+
import ReportActionAvatars from './ReportActionAvatars';
4437
import type {TransactionListItemType} from './SelectionList/types';
45-
import SubscriptAvatar from './SubscriptAvatar';
4638
import Text from './Text';
4739

4840
type AvatarWithDisplayNameProps = {
4941
/** The report currently being looked at */
5042
report: OnyxEntry<Report>;
5143

52-
/** The policy which the user has access to and which the report is tied to */
53-
policy?: OnyxEntry<Policy>;
54-
5544
/** The size of the avatar */
5645
size?: ValueOf<typeof CONST.AVATAR_SIZE>;
5746

@@ -75,16 +64,6 @@ type AvatarWithDisplayNameProps = {
7564

7665
/** Color of the secondary avatar border, usually should match the container background */
7766
avatarBorderColor?: ColorValue;
78-
79-
/** If we want to override the default avatar behavior and set a single avatar, we should pass this prop. */
80-
singleAvatarDetails?: ReportAvatarDetails;
81-
};
82-
83-
const fallbackIcon: Icon = {
84-
source: FallbackAvatar,
85-
type: CONST.ICON_TYPE_AVATAR,
86-
name: '',
87-
id: -1,
8867
};
8968

9069
function getCustomDisplayName(
@@ -164,15 +143,13 @@ function getCustomDisplayName(
164143
}
165144

166145
function AvatarWithDisplayName({
167-
policy,
168146
report,
169147
isAnonymous = false,
170148
size = CONST.AVATAR_SIZE.DEFAULT,
171149
shouldEnableDetailPageNavigation = false,
172150
shouldEnableAvatarNavigation = true,
173151
shouldUseCustomSearchTitleName = false,
174152
transactions = [],
175-
singleAvatarDetails,
176153
openParentReportInCurrentTab = false,
177154
avatarBorderColor: avatarBorderColorProp,
178155
}: AvatarWithDisplayNameProps) {
@@ -190,13 +167,10 @@ function AvatarWithDisplayName({
190167
const parentReportActionParam = report?.parentReportActionID ? parentReportActions?.[report.parentReportActionID] : undefined;
191168
const title = getReportName(report, undefined, parentReportActionParam, personalDetails, invoiceReceiverPolicy, reportAttributes);
192169
const subtitle = getChatRoomSubtitle(report, {isCreateExpenseFlow: true});
193-
const parentNavigationSubtitleData = getParentNavigationSubtitle(report, policy);
170+
const parentNavigationSubtitleData = getParentNavigationSubtitle(report);
194171
const isMoneyRequestOrReport = isMoneyRequestReport(report) || isMoneyRequest(report) || isTrackExpenseReport(report) || isInvoiceReport(report);
195-
const isReportArchived = useReportIsArchived(report?.reportID);
196-
const icons = getIcons(report, personalDetails, null, '', -1, policy, invoiceReceiverPolicy, isReportArchived);
197172
const ownerPersonalDetails = getPersonalDetailsForAccountIDs(report?.ownerAccountID ? [report.ownerAccountID] : [], personalDetails);
198173
const displayNamesWithTooltips = getDisplayNamesWithTooltips(Object.values(ownerPersonalDetails), false);
199-
const shouldShowSubscriptAvatar = shouldReportShowSubscript(report, isReportArchived);
200174
const avatarBorderColor = avatarBorderColorProp ?? (isAnonymous ? theme.highlightBG : theme.componentBG);
201175

202176
const actorAccountID = useRef<number | null>(null);
@@ -245,65 +219,34 @@ function AvatarWithDisplayName({
245219

246220
const shouldUseFullTitle = isMoneyRequestOrReport || isAnonymous;
247221

248-
const getAvatar = useCallback(() => {
249-
if (shouldShowSubscriptAvatar) {
250-
return (
251-
<SubscriptAvatar
252-
backgroundColor={avatarBorderColor}
253-
mainAvatar={icons.at(0) ?? fallbackIcon}
254-
secondaryAvatar={icons.at(1)}
255-
size={size}
256-
/>
257-
);
258-
}
259-
260-
if (!singleAvatarDetails || singleAvatarDetails.shouldDisplayAllActors || !singleAvatarDetails.reportPreviewSenderID) {
261-
return (
262-
<MultipleAvatars
263-
icons={icons}
264-
size={size}
265-
secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(avatarBorderColor)]}
266-
/>
267-
);
268-
}
269-
270-
return (
271-
<SingleReportAvatar
272-
reportPreviewDetails={singleAvatarDetails}
273-
personalDetails={personalDetails}
274-
containerStyles={[styles.actionAvatar, styles.mr3]}
275-
actorAccountID={singleAvatarDetails.reportPreviewSenderID}
276-
/>
277-
);
278-
}, [StyleUtils, avatarBorderColor, icons, personalDetails, shouldShowSubscriptAvatar, singleAvatarDetails, size, styles]);
279-
280-
const getWrappedAvatar = useCallback(() => {
281-
const avatar = getAvatar();
282-
283-
if (!shouldEnableAvatarNavigation) {
284-
return <View accessibilityLabel={title}>{avatar}</View>;
285-
}
286-
287-
return (
288-
<View accessibilityLabel={title}>
289-
<PressableWithoutFeedback
290-
onPress={showActorDetails}
291-
accessibilityLabel={title}
292-
role={getButtonRole(true)}
293-
>
294-
{avatar}
295-
</PressableWithoutFeedback>
296-
</View>
297-
);
298-
}, [getAvatar, shouldEnableAvatarNavigation, showActorDetails, title]);
299-
300-
const WrappedAvatar = getWrappedAvatar();
222+
const multipleAvatars = (
223+
<ReportActionAvatars
224+
singleAvatarContainerStyle={[styles.actionAvatar, styles.mr3]}
225+
subscriptAvatarBorderColor={avatarBorderColor}
226+
size={size}
227+
secondaryAvatarContainerStyle={StyleUtils.getBackgroundAndBorderStyle(avatarBorderColor)}
228+
reportID={report?.reportID}
229+
/>
230+
);
301231

302232
const headerView = (
303233
<View style={[styles.appContentHeaderTitle, styles.flex1]}>
304234
{!!report && !!title && (
305235
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween]}>
306-
{WrappedAvatar}
236+
<View accessibilityLabel={title}>
237+
{shouldEnableAvatarNavigation ? (
238+
<PressableWithoutFeedback
239+
onPress={showActorDetails}
240+
accessibilityLabel={title}
241+
role={getButtonRole(true)}
242+
>
243+
{multipleAvatars}
244+
</PressableWithoutFeedback>
245+
) : (
246+
multipleAvatars
247+
)}
248+
</View>
249+
307250
<View style={[styles.flex1, styles.flexColumn]}>
308251
{getCustomDisplayName(
309252
shouldUseCustomSearchTitleName,

src/components/HeaderWithBackButton/index.tsx

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,7 @@ function HeaderWithBackButton({
3535
onDownloadButtonPress = () => {},
3636
onThreeDotsButtonPress = () => {},
3737
report,
38-
policy,
3938
policyAvatar,
40-
singleAvatarDetails,
4139
shouldShowReportAvatarWithDisplay = false,
4240
shouldShowBackButton = true,
4341
shouldShowBorderBottom = false,
@@ -103,8 +101,6 @@ function HeaderWithBackButton({
103101
return (
104102
<AvatarWithDisplayName
105103
report={report}
106-
policy={policy}
107-
singleAvatarDetails={singleAvatarDetails}
108104
shouldEnableDetailPageNavigation={shouldEnableDetailPageNavigation}
109105
openParentReportInCurrentTab={openParentReportInCurrentTab}
110106
/>
@@ -124,7 +120,6 @@ function HeaderWithBackButton({
124120
StyleUtils,
125121
subTitleLink,
126122
shouldUseHeadlineHeader,
127-
policy,
128123
progressBarPercentage,
129124
report,
130125
shouldEnableDetailPageNavigation,
@@ -140,7 +135,6 @@ function HeaderWithBackButton({
140135
titleColor,
141136
translate,
142137
openParentReportInCurrentTab,
143-
singleAvatarDetails,
144138
]);
145139
const ThreeDotMenuButton = useMemo(() => {
146140
if (shouldShowThreeDotsButton) {

src/components/HeaderWithBackButton/types.ts

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,11 @@ import type {ReactNode} from 'react';
22
import type {StyleProp, ViewStyle} from 'react-native';
33
import type {OnyxEntry} from 'react-native-onyx';
44
import type {PopoverMenuItem} from '@components/PopoverMenu';
5-
import type {ReportAvatarDetails} from '@hooks/useReportAvatarDetails';
65
import type {Action} from '@hooks/useSingleExecution';
76
import type {StepCounterParams} from '@src/languages/params';
87
import type {TranslationPaths} from '@src/languages/types';
98
import type {AnchorPosition} from '@src/styles';
10-
import type {Policy, Report} from '@src/types/onyx';
9+
import type {Report} from '@src/types/onyx';
1110
import type {Icon} from '@src/types/onyx/OnyxCommon';
1211
import type AnchorAlignment from '@src/types/utils/AnchorAlignment';
1312
import type ChildrenProps from '@src/types/utils/ChildrenProps';
@@ -116,9 +115,6 @@ type HeaderWithBackButtonProps = Partial<ChildrenProps> & {
116115
/** Report, if we're showing the details for one and using AvatarWithDisplay */
117116
report?: OnyxEntry<Report>;
118117

119-
/** The report's policy, if we're showing the details for a report and need info about it for AvatarWithDisplay */
120-
policy?: OnyxEntry<Policy>;
121-
122118
/** Single execution function to prevent concurrent navigation actions */
123119
singleExecution?: <T extends unknown[]>(action: Action<T>) => Action<T>;
124120

@@ -162,9 +158,6 @@ type HeaderWithBackButtonProps = Partial<ChildrenProps> & {
162158
shouldMinimizeMenuButton?: boolean;
163159
/** Whether to open the parent report link in the current tab if possible */
164160
openParentReportInCurrentTab?: boolean;
165-
166-
/** If we want to override the default avatar behavior and set a single avatar, we should pass this prop. */
167-
singleAvatarDetails?: ReportAvatarDetails;
168161
};
169162

170163
export type {ThreeDotsMenuItem};

0 commit comments

Comments
 (0)