Skip to content

Commit 4a5b141

Browse files
authored
Merge pull request Expensify#64802 from software-mansion-labs/korytko/display-sender-avatar-next-to-preview
[Avatars - Reports] If a report contains the expenses of one person, only display their avatar + fix `isOneTransactionReport`
2 parents 861d3e6 + 9ae3fac commit 4a5b141

14 files changed

Lines changed: 1164 additions & 177 deletions

File tree

__mocks__/reportData/personalDetails.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import type {PersonalDetailsList} from '@src/types/onyx';
22

33
const usersIDs = [15593135, 51760358, 26502375] as const;
44

5-
const personalDetails: PersonalDetailsList = {
5+
const personalDetails = {
66
[usersIDs[0]]: {
77
accountID: usersIDs[0],
88
avatar: '@assets/images/avatars/user/default-avatar_1.svg',
@@ -63,6 +63,6 @@ const personalDetails: PersonalDetailsList = {
6363
phoneNumber: '33333333',
6464
validated: true,
6565
},
66-
};
66+
} satisfies PersonalDetailsList;
6767

6868
export default personalDetails;

src/components/AvatarWithDisplayName.tsx

Lines changed: 67 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ 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';
78
import useReportIsArchived from '@hooks/useReportIsArchived';
89
import useStyleUtils from '@hooks/useStyleUtils';
910
import useTheme from '@hooks/useTheme';
@@ -39,6 +40,7 @@ import {FallbackAvatar} from './Icon/Expensicons';
3940
import MultipleAvatars from './MultipleAvatars';
4041
import ParentNavigationSubtitle from './ParentNavigationSubtitle';
4142
import PressableWithoutFeedback from './Pressable/PressableWithoutFeedback';
43+
import SingleReportAvatar from './ReportActionItem/SingleReportAvatar';
4244
import type {TransactionListItemType} from './SelectionList/types';
4345
import SubscriptAvatar from './SubscriptAvatar';
4446
import Text from './Text';
@@ -73,6 +75,9 @@ type AvatarWithDisplayNameProps = {
7375

7476
/** Color of the secondary avatar border, usually should match the container background */
7577
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;
7681
};
7782

7883
const fallbackIcon: Icon = {
@@ -167,6 +172,7 @@ function AvatarWithDisplayName({
167172
shouldEnableAvatarNavigation = true,
168173
shouldUseCustomSearchTitleName = false,
169174
transactions = [],
175+
singleAvatarDetails,
170176
openParentReportInCurrentTab = false,
171177
avatarBorderColor: avatarBorderColorProp,
172178
}: AvatarWithDisplayNameProps) {
@@ -236,40 +242,74 @@ function AvatarWithDisplayName({
236242
Navigation.navigate(ROUTES.REPORT_WITH_ID_DETAILS.getRoute(report.reportID));
237243
}
238244
}, [report, shouldEnableDetailPageNavigation, goToDetailsPage]);
245+
239246
const shouldUseFullTitle = isMoneyRequestOrReport || isAnonymous;
240-
const avatar = (
241-
<View accessibilityLabel={title}>
242-
{shouldShowSubscriptAvatar ? (
243-
<SubscriptAvatar
244-
backgroundColor={avatarBorderColor}
245-
mainAvatar={icons.at(0) ?? fallbackIcon}
246-
secondaryAvatar={icons.at(1)}
247-
size={size}
248-
/>
249-
) : (
250-
<MultipleAvatars
251-
icons={icons}
252-
size={size}
253-
secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(avatarBorderColor)]}
247+
248+
const getAvatar = useCallback(
249+
(accountID: number) => {
250+
if (shouldShowSubscriptAvatar) {
251+
return (
252+
<SubscriptAvatar
253+
backgroundColor={avatarBorderColor}
254+
mainAvatar={icons.at(0) ?? fallbackIcon}
255+
secondaryAvatar={icons.at(1)}
256+
size={size}
257+
/>
258+
);
259+
}
260+
261+
if (!singleAvatarDetails || singleAvatarDetails.shouldDisplayAllActors || !singleAvatarDetails.reportPreviewSenderID) {
262+
return (
263+
<MultipleAvatars
264+
icons={icons}
265+
size={size}
266+
secondAvatarStyle={[StyleUtils.getBackgroundAndBorderStyle(avatarBorderColor)]}
267+
/>
268+
);
269+
}
270+
271+
return (
272+
<SingleReportAvatar
273+
reportPreviewDetails={singleAvatarDetails}
274+
personalDetails={personalDetails}
275+
containerStyles={[styles.actionAvatar, styles.mr3]}
276+
actorAccountID={accountID}
254277
/>
255-
)}
256-
</View>
278+
);
279+
},
280+
[StyleUtils, avatarBorderColor, icons, personalDetails, shouldShowSubscriptAvatar, singleAvatarDetails, size, styles],
257281
);
282+
283+
const getWrappedAvatar = useCallback(
284+
(accountID: number) => {
285+
const avatar = getAvatar(accountID);
286+
287+
if (!shouldEnableAvatarNavigation) {
288+
return <View accessibilityLabel={title}>{avatar}</View>;
289+
}
290+
291+
return (
292+
<View accessibilityLabel={title}>
293+
<PressableWithoutFeedback
294+
onPress={showActorDetails}
295+
accessibilityLabel={title}
296+
role={getButtonRole(true)}
297+
>
298+
{avatar}
299+
</PressableWithoutFeedback>
300+
</View>
301+
);
302+
},
303+
[getAvatar, shouldEnableAvatarNavigation, showActorDetails, title],
304+
);
305+
306+
const WrappedAvatar = getWrappedAvatar(actorAccountID?.current ?? CONST.DEFAULT_NUMBER_ID);
307+
258308
const headerView = (
259309
<View style={[styles.appContentHeaderTitle, styles.flex1]}>
260310
{!!report && !!title && (
261311
<View style={[styles.flex1, styles.flexRow, styles.alignItemsCenter, styles.justifyContentBetween]}>
262-
{shouldEnableAvatarNavigation ? (
263-
<PressableWithoutFeedback
264-
onPress={showActorDetails}
265-
accessibilityLabel={title}
266-
role={getButtonRole(true)}
267-
>
268-
{avatar}
269-
</PressableWithoutFeedback>
270-
) : (
271-
avatar
272-
)}
312+
{WrappedAvatar}
273313
<View style={[styles.flex1, styles.flexColumn]}>
274314
{getCustomDisplayName(
275315
shouldUseCustomSearchTitleName,

src/components/HeaderWithBackButton/index.tsx

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ function HeaderWithBackButton({
3737
report,
3838
policy,
3939
policyAvatar,
40+
singleAvatarDetails,
4041
shouldShowReportAvatarWithDisplay = false,
4142
shouldShowBackButton = true,
4243
shouldShowBorderBottom = false,
@@ -103,6 +104,7 @@ function HeaderWithBackButton({
103104
<AvatarWithDisplayName
104105
report={report}
105106
policy={policy}
107+
singleAvatarDetails={singleAvatarDetails}
106108
shouldEnableDetailPageNavigation={shouldEnableDetailPageNavigation}
107109
openParentReportInCurrentTab={openParentReportInCurrentTab}
108110
/>
@@ -138,6 +140,7 @@ function HeaderWithBackButton({
138140
titleColor,
139141
translate,
140142
openParentReportInCurrentTab,
143+
singleAvatarDetails,
141144
]);
142145
const ThreeDotMenuButton = useMemo(() => {
143146
if (shouldShowThreeDotsButton) {

src/components/HeaderWithBackButton/types.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ 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';
56
import type {Action} from '@hooks/useSingleExecution';
67
import type {StepCounterParams} from '@src/languages/params';
78
import type {TranslationPaths} from '@src/languages/types';
@@ -161,6 +162,9 @@ type HeaderWithBackButtonProps = Partial<ChildrenProps> & {
161162
shouldMinimizeMenuButton?: boolean;
162163
/** Whether to open the parent report link in the current tab if possible */
163164
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;
164168
};
165169

166170
export type {ThreeDotsMenuItem};

src/components/MoneyReportHeader.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import useNetwork from '@hooks/useNetwork';
1010
import useOnyx from '@hooks/useOnyx';
1111
import usePaymentAnimations from '@hooks/usePaymentAnimations';
1212
import usePaymentOptions from '@hooks/usePaymentOptions';
13+
import useReportAvatarDetails from '@hooks/useReportAvatarDetails';
1314
import useReportIsArchived from '@hooks/useReportIsArchived';
1415
import useResponsiveLayout from '@hooks/useResponsiveLayout';
1516
import useSelectedTransactionsActions from '@hooks/useSelectedTransactionsActions';
@@ -185,6 +186,15 @@ function MoneyReportHeader({
185186
const isExported = isExportedUtils(reportActions);
186187
const integrationNameFromExportMessage = isExported ? getIntegrationNameFromExportMessageUtils(reportActions) : null;
187188

189+
const [reportPreviewAction] = useOnyx(`${ONYXKEYS.COLLECTION.REPORT_ACTIONS}${chatReport?.reportID}`, {
190+
canBeMissing: true,
191+
selector: (actions) => Object.entries(actions ?? {}).find(([id]) => id === moneyRequestReport?.parentReportActionID)?.[1],
192+
});
193+
194+
const [personalDetails] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
195+
canBeMissing: true,
196+
});
197+
188198
const [downloadErrorModalVisible, setDownloadErrorModalVisible] = useState(false);
189199
const [isCancelPaymentModalVisible, setIsCancelPaymentModalVisible] = useState(false);
190200
const [isDeleteExpenseModalVisible, setIsDeleteExpenseModalVisible] = useState(false);
@@ -220,6 +230,8 @@ function MoneyReportHeader({
220230
[allViolations, transactionIDs],
221231
);
222232

233+
const details = useReportAvatarDetails({report: chatReport, iouReport: moneyRequestReport, action: reportPreviewAction, policy, innerPolicies: policies, personalDetails});
234+
223235
const messagePDF = useMemo(() => {
224236
if (!reportPDFFilename) {
225237
return translate('reportDetailsPage.waitForPDF');
@@ -944,6 +956,7 @@ function MoneyReportHeader({
944956
<HeaderWithBackButton
945957
shouldShowReportAvatarWithDisplay
946958
shouldShowPinButton={false}
959+
singleAvatarDetails={details}
947960
report={moneyRequestReport}
948961
policy={policy}
949962
shouldShowBackButton={shouldShowBackButton}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import React from 'react';
2+
import type {ViewStyle} from 'react-native';
3+
import {View} from 'react-native';
4+
import Avatar from '@components/Avatar';
5+
import UserDetailsTooltip from '@components/UserDetailsTooltip';
6+
import type {ReportAvatarDetails} from '@hooks/useReportAvatarDetails';
7+
import CONST from '@src/CONST';
8+
import type {PersonalDetailsList} from '@src/types/onyx';
9+
10+
function SingleReportAvatar({
11+
reportPreviewDetails,
12+
personalDetails,
13+
containerStyles,
14+
actorAccountID,
15+
}: {
16+
reportPreviewDetails: ReportAvatarDetails;
17+
personalDetails: PersonalDetailsList | undefined;
18+
containerStyles: ViewStyle[];
19+
actorAccountID: number | null | undefined;
20+
}) {
21+
const {primaryAvatar, isWorkspaceActor, fallbackIcon: reportFallbackIcon, reportPreviewAction} = reportPreviewDetails;
22+
const delegatePersonalDetails = reportPreviewAction?.delegateAccountID ? personalDetails?.[reportPreviewAction?.delegateAccountID] : undefined;
23+
24+
return (
25+
<UserDetailsTooltip
26+
accountID={Number(delegatePersonalDetails && !isWorkspaceActor ? actorAccountID : (primaryAvatar.id ?? CONST.DEFAULT_NUMBER_ID))}
27+
delegateAccountID={reportPreviewAction?.delegateAccountID}
28+
icon={primaryAvatar}
29+
>
30+
<View>
31+
<Avatar
32+
containerStyles={containerStyles}
33+
source={primaryAvatar.source}
34+
type={primaryAvatar.type}
35+
name={primaryAvatar.name}
36+
avatarID={primaryAvatar.id}
37+
fallbackIcon={reportFallbackIcon}
38+
/>
39+
</View>
40+
</UserDetailsTooltip>
41+
);
42+
}
43+
44+
export default SingleReportAvatar;

src/components/ReportActionItem/TransactionPreview/types.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,7 @@ type TransactionPreviewContentProps = {
105105
/** Holds the transaction data entry from Onyx */
106106
transaction: OnyxEntry<Transaction>;
107107

108-
/** The original amount value on the transaction. This is used to deduce who is the sender and who is the receiver of the money request
108+
/** The amount of the transaction saved in the database. This is used to deduce who is the sender and who is the receiver of the money request
109109
* In case of Splits the property `transaction` is actually an original transaction (for the whole split) and it does not have the data required to deduce who is the sender */
110110
transactionRawAmount: number;
111111

0 commit comments

Comments
 (0)