Skip to content

Commit 3e8642c

Browse files
authored
Merge pull request Expensify#96269 from Expensify/georgia-letter-avatars-custom
Let users pick a letter avatar color
2 parents b95999f + 5916ffd commit 3e8642c

26 files changed

Lines changed: 1127 additions & 781 deletions

config/eslint/eslint.seatbelt.tsv

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1864,7 +1864,6 @@
18641864
"../../tests/ui/AgentsPromoBannersTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
18651865
"../../tests/ui/AssignCardFeed.tsx" "@typescript-eslint/no-unsafe-type-assertion" 3
18661866
"../../tests/ui/AuthScreensInitHandlerTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 7
1867-
"../../tests/ui/AvatarSelector.test.tsx" "@typescript-eslint/no-unsafe-type-assertion" 7
18681867
"../../tests/ui/BaseListItemTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
18691868
"../../tests/ui/BaseLoginFormTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 1
18701869
"../../tests/ui/BaseModalTest.tsx" "@typescript-eslint/no-unsafe-type-assertion" 2

src/components/Avatar.tsx

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,22 @@
11
import useDefaultAvatars from '@hooks/useDefaultAvatars';
22
import useNetwork from '@hooks/useNetwork';
3+
import useOnyx from '@hooks/useOnyx';
34
import useStyleUtils from '@hooks/useStyleUtils';
45
import useTheme from '@hooks/useTheme';
56
import useThemeStyles from '@hooks/useThemeStyles';
67

78
import {findLocalAvatarForURL} from '@libs/Avatars/AvatarLookup';
9+
import type {LetterAvatarColorStyle} from '@libs/Avatars/letterAvatarPalette';
10+
import {isLetterAvatarSchemeKey, LETTER_AVATAR_SCHEMES} from '@libs/Avatars/letterAvatarPalette';
811
import {getDefaultWorkspaceAvatar, getDefaultWorkspaceAvatarTestID} from '@libs/ReportUtils';
912
import type {AvatarSource} from '@libs/UserAvatarUtils';
1013
import {getAvatar, parseLetterAvatarURL} from '@libs/UserAvatarUtils';
1114

1215
import type {AvatarSizeName} from '@styles/utils';
1316

1417
import CONST from '@src/CONST';
18+
import ONYXKEYS from '@src/ONYXKEYS';
19+
import {avatarStyleColorSelector} from '@src/selectors/PersonalDetails';
1520
import type {AvatarType} from '@src/types/onyx/OnyxCommon';
1621

1722
import type {ImageStyle, StyleProp, ViewStyle} from 'react-native';
@@ -66,6 +71,36 @@ type AvatarProps = {
6671
testID?: string;
6772
};
6873

74+
type UserLetterAvatarProps = {
75+
/** Initials parsed from the generated letter-avatar URL */
76+
initials: string;
77+
78+
/** Colors encoded in the generated letter-avatar URL */
79+
urlColors: LetterAvatarColorStyle;
80+
81+
/** Account whose picked avatarStyle color overrides the URL colors */
82+
accountID: number | undefined;
83+
84+
/** Avatar size in pixels */
85+
size: number;
86+
};
87+
88+
function UserLetterAvatar({initials, urlColors, accountID, size}: UserLetterAvatarProps) {
89+
// A picked avatarStyle color is authoritative over the color encoded in the URL.
90+
const [pickedColorKey] = useOnyx(ONYXKEYS.PERSONAL_DETAILS_LIST, {
91+
selector: avatarStyleColorSelector(accountID),
92+
});
93+
const colors = pickedColorKey && isLetterAvatarSchemeKey(pickedColorKey) ? LETTER_AVATAR_SCHEMES[pickedColorKey] : urlColors;
94+
95+
return (
96+
<UserInitialsAvatar
97+
text={initials}
98+
colors={colors}
99+
size={size}
100+
/>
101+
);
102+
}
103+
69104
function Avatar({
70105
source: originalSource,
71106
imageStyles,
@@ -92,7 +127,13 @@ function Avatar({
92127
const isWorkspace = type === CONST.ICON_TYPE_WORKSPACE;
93128
const userAccountID = isWorkspace ? undefined : (avatarID as number);
94129

95-
const source = isWorkspace ? originalSource : getAvatar({avatarSource: originalSource, accountID: userAccountID, defaultAvatars});
130+
const source = isWorkspace
131+
? originalSource
132+
: getAvatar({
133+
avatarSource: originalSource,
134+
accountID: userAccountID,
135+
defaultAvatars,
136+
});
96137

97138
// Read the color and initials directly from the generated letter-avatar URL.
98139
const letterAvatarParts = parseLetterAvatarURL(source);
@@ -130,9 +171,10 @@ function Avatar({
130171
testID={testID}
131172
>
132173
<View style={[iconStyle, StyleUtils.getAvatarBorderStyle(size, type), iconAdditionalStyles]}>
133-
<UserInitialsAvatar
134-
text={letterAvatarParts.initials}
135-
colors={letterAvatarParts.colors}
174+
<UserLetterAvatar
175+
initials={letterAvatarParts.initials}
176+
urlColors={letterAvatarParts.colors}
177+
accountID={userAccountID}
136178
size={iconSize}
137179
/>
138180
</View>

src/components/AvatarSelector.tsx

Lines changed: 27 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import useLetterAvatars from '@hooks/useLetterAvatars';
22
import useLocalize from '@hooks/useLocalize';
3-
import usePermissions from '@hooks/usePermissions';
43
import useStyleUtils from '@hooks/useStyleUtils';
54
import useThemeStyles from '@hooks/useThemeStyles';
65

@@ -16,6 +15,7 @@ import {View} from 'react-native';
1615
import Avatar from './Avatar';
1716
import {PressableWithFeedback} from './Pressable';
1817
import Text from './Text';
18+
import UserInitialsAvatar from './UserInitialsAvatar';
1919

2020
type AvatarSelectorProps = {
2121
/** Currently selected avatar ID */
@@ -24,9 +24,6 @@ type AvatarSelectorProps = {
2424
/** Called when an avatar is selected */
2525
onSelect: (id: string) => void;
2626

27-
/** Used to generate letter avatars */
28-
name?: string;
29-
3027
/** Optional: size of avatars in grid */
3128
size?: AvatarSizeName;
3229

@@ -39,18 +36,17 @@ const SPACER_SIZE = 10;
3936
/**
4037
* AvatarSelector — renders a grid of selectable avatars.
4138
*/
42-
function AvatarSelector({selectedID, onSelect, label, name, size = CONST.AVATAR_SIZE.MEDIUM}: AvatarSelectorProps) {
39+
function AvatarSelector({selectedID, onSelect, label, size = CONST.AVATAR_SIZE.MEDIUM}: AvatarSelectorProps) {
4340
const {translate} = useLocalize();
4441
const styles = useThemeStyles();
4542
const StyleUtils = useStyleUtils();
46-
const {avatarList} = useLetterAvatars(name, size);
47-
const {isBetaEnabled} = usePermissions();
43+
const {initials, options: letterAvatarOptions} = useLetterAvatars();
4844

4945
const iconSize = StyleUtils.getAvatarSize(size);
5046

5147
return (
5248
<>
53-
{!!label && avatarList?.length > 0 && (
49+
{!!label && letterAvatarOptions.length > 0 && (
5450
<Text style={StyleUtils.combineStyles([styles.sidebarLinkText, styles.optionAlternateText, styles.textLabelSupporting, styles.pre, styles.ph2])}>{label}</Text>
5551
)}
5652
<View style={styles.avatarSelectorListContainer}>
@@ -76,29 +72,31 @@ function AvatarSelector({selectedID, onSelect, label, name, size = CONST.AVATAR_
7672
</PressableWithFeedback>
7773
);
7874
})}
79-
{isBetaEnabled(CONST.BETAS.DEFAULT_LETTER_AVATARS) &&
80-
avatarList.map(({id, StyledLetterAvatar}) => {
81-
const isSelected = selectedID === id;
82-
83-
return (
84-
<PressableWithFeedback
85-
key={id}
86-
accessible
87-
accessibilityRole="button"
88-
accessibilityLabel={translate('avatarPage.selectAvatar')}
89-
onPress={() => onSelect(id)}
90-
style={[styles.avatarSelectorWrapper, isSelected && styles.avatarSelected]}
75+
{letterAvatarOptions.map(({id, colors}) => {
76+
const isSelected = selectedID === id;
77+
78+
return (
79+
<PressableWithFeedback
80+
key={id}
81+
accessible
82+
accessibilityRole="button"
83+
accessibilityLabel={translate('avatarPage.selectAvatar')}
84+
onPress={() => onSelect(id)}
85+
style={[styles.avatarSelectorWrapper, isSelected && styles.avatarSelected]}
86+
>
87+
<View
88+
style={styles.avatarSelectorContainer}
89+
testID={`AvatarSelector_${id}`}
9190
>
92-
<Avatar
93-
type={CONST.ICON_TYPE_AVATAR}
94-
source={StyledLetterAvatar}
95-
size={size}
96-
containerStyles={styles.avatarSelectorContainer}
97-
testID={`AvatarSelector_${id}`}
91+
<UserInitialsAvatar
92+
text={initials}
93+
colors={colors}
94+
size={iconSize}
9895
/>
99-
</PressableWithFeedback>
100-
);
101-
})}
96+
</View>
97+
</PressableWithFeedback>
98+
);
99+
})}
102100
{/* We need to add several invisible items at the end of the avatar list to guarantee that the last row avatars are aligned properly */}
103101
{[...Array(SPACER_SIZE).keys()].map((i) => (
104102
<View

src/components/ColoredLetterAvatar.tsx

Lines changed: 0 additions & 47 deletions
This file was deleted.

src/hooks/useLetterAvatars.ts

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import type {LetterAvatarColorStyle, LetterAvatarSchemeKey} from '@libs/Avatars/letterAvatarPalette';
2+
import {LETTER_AVATAR_COLOR_KEYS, LETTER_AVATAR_SCHEMES} from '@libs/Avatars/letterAvatarPalette';
3+
import {getLetterAvatarInitials} from '@libs/UserAvatarUtils';
4+
5+
import useCurrentUserPersonalDetails from './useCurrentUserPersonalDetails';
6+
7+
type LetterAvatarOption = {
8+
/** The color-scheme key, persisted when the user picks this option */
9+
id: LetterAvatarSchemeKey;
10+
11+
/** Background and fill colors for the option */
12+
colors: LetterAvatarColorStyle;
13+
};
14+
15+
type LetterAvatarsResult = {
16+
/** The current user's letter-avatar initials, or '' when no letter avatar applies */
17+
initials: string;
18+
19+
/** One option per palette scheme, empty when there are no initials */
20+
options: LetterAvatarOption[];
21+
};
22+
23+
/**
24+
* Returns the current user's letter-avatar initials and one selectable option per palette color scheme.
25+
*/
26+
function useLetterAvatars(): LetterAvatarsResult {
27+
const {firstName, lastName, login, email} = useCurrentUserPersonalDetails();
28+
const initialsLogin = login?.length ? login : email;
29+
const initials = getLetterAvatarInitials(firstName ?? '', lastName ?? '', initialsLogin ?? '');
30+
31+
if (initials === '') {
32+
return {initials, options: []};
33+
}
34+
35+
return {
36+
initials,
37+
options: LETTER_AVATAR_COLOR_KEYS.map((id) => ({
38+
id,
39+
colors: LETTER_AVATAR_SCHEMES[id],
40+
})),
41+
};
42+
}
43+
44+
export default useLetterAvatars;

src/hooks/useLetterAvatars.tsx

Lines changed: 0 additions & 80 deletions
This file was deleted.
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
1+
import type {LetterAvatarSchemeKey} from '@libs/Avatars/letterAvatarPalette';
12
import type {CustomRNImageManipulatorResult} from '@libs/cropOrRotateImage/types';
23

34
type UpdateUserAvatarParams = {
45
file?: File | CustomRNImageManipulatorResult;
56
customExpensifyAvatarID?: string;
7+
color?: LetterAvatarSchemeKey;
68
};
79

810
export default UpdateUserAvatarParams;

0 commit comments

Comments
 (0)