Skip to content

Commit 36b1795

Browse files
M-i-k-e-lethanshar
andauthored
Avatar - add name with initials and backgroundColor specific to that name (#1239)
* Avatar - add name with initials and backgroundColor specific to that name * Add typings * Renaming * Remove getInitials and getBackgroundColor from the API * Do not enter memoized function when not needed * Add tests for uut.getInitials with limit * Add tests (and fix) getBackgroundColor * useAutoColorsConfig --> useAutoColors * Use grey80 and not the deprecated dark80 Co-authored-by: Ethan Sharabi <[email protected]> Co-authored-by: Ethan Sharabi <[email protected]>
1 parent e2ca7fa commit 36b1795

File tree

5 files changed

+170
-11
lines changed

5 files changed

+170
-11
lines changed

generatedTypes/components/avatar/index.d.ts

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,20 @@ export declare enum BadgePosition {
1414
BOTTOM_RIGHT = "BOTTOM_RIGHT",
1515
BOTTOM_LEFT = "BOTTOM_LEFT"
1616
}
17+
export declare type AutoColorsProps = {
18+
/**
19+
* Avatar colors to be used when useAutoColors is true
20+
*/
21+
avatarColors?: string[];
22+
/**
23+
* Replace the default hashing function (name -> number)
24+
*/
25+
hashFunction?: (name?: string) => number;
26+
/**
27+
* Background color in cases where the getBackgroundColor returns undefined.
28+
*/
29+
defaultColor?: string;
30+
};
1731
export declare type AvatarProps = Pick<AccessibilityProps, 'accessibilityLabel'> & {
1832
/**
1933
* Adds fade in animation when Avatar image loads
@@ -63,6 +77,21 @@ export declare type AvatarProps = Pick<AccessibilityProps, 'accessibilityLabel'>
6377
* fails (equiv. to Image.onError()).
6478
*/
6579
onImageLoadError?: ImagePropsBase['onError'];
80+
/**
81+
* The name of the avatar user.
82+
* If no label is provided, the initials will be generated from the name.
83+
* autoColorsConfig will use the name to create the background color of the Avatar.
84+
*/
85+
name?: string;
86+
/**
87+
* Hash the name (or label) to get a color, so each name will have a specific color.
88+
* Default is false.
89+
*/
90+
useAutoColors?: boolean;
91+
/**
92+
* Send this to use the name to infer a backgroundColor
93+
*/
94+
autoColorsConfig?: AutoColorsProps;
6695
/**
6796
* Label that can represent initials
6897
*/
@@ -125,7 +154,6 @@ declare class Avatar extends PureComponent<AvatarProps> {
125154
static badgePosition: typeof BadgePosition;
126155
static defaultProps: {
127156
animate: boolean;
128-
backgroundColor: string;
129157
size: number;
130158
labelColor: string;
131159
badgePosition: BadgePosition;
@@ -141,6 +169,10 @@ declare class Avatar extends PureComponent<AvatarProps> {
141169
renderBadge(): JSX.Element | undefined;
142170
renderRibbon(): JSX.Element | undefined;
143171
renderImage(): JSX.Element | undefined;
172+
getText: (this: any, label: any, name: any) => any;
173+
get text(): any;
174+
getBackgroundColor: (this: any, text: any, avatarColors: any, hashFunction: any, defaultColor: any) => string | undefined;
175+
get backgroundColor(): string | undefined;
144176
render(): JSX.Element;
145177
}
146178
declare function createStyles(props: AvatarProps): {
@@ -211,6 +243,21 @@ declare const _default: React.ComponentClass<Pick<AccessibilityProps, "accessibi
211243
* fails (equiv. to Image.onError()).
212244
*/
213245
onImageLoadError?: ((error: import("react-native").NativeSyntheticEvent<import("react-native").ImageErrorEventData>) => void) | undefined;
246+
/**
247+
* The name of the avatar user.
248+
* If no label is provided, the initials will be generated from the name.
249+
* autoColorsConfig will use the name to create the background color of the Avatar.
250+
*/
251+
name?: string | undefined;
252+
/**
253+
* Hash the name (or label) to get a color, so each name will have a specific color.
254+
* Default is false.
255+
*/
256+
useAutoColors?: boolean | undefined;
257+
/**
258+
* Send this to use the name to infer a backgroundColor
259+
*/
260+
autoColorsConfig?: AutoColorsProps | undefined;
214261
/**
215262
* Label that can represent initials
216263
*/
Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
1+
export declare function hashStringToNumber(str: string): number;
12
export declare function getAvatarColors(): string[];
23
export declare function getColorById(id: string, avatarColors?: string[]): string;
3-
export declare function getInitials(name: string): string;
4+
export declare function getInitials(name?: string, limit?: number): string;
5+
export declare function getBackgroundColor(name?: string, avatarColors?: string[], hashFunction?: (name?: string) => number, defaultColor?: string): string | undefined;
46
export declare function isGravatarUrl(url: string): any;
57
export declare function isBlankGravatarUrl(url: string): boolean;
68
export declare function patchGravatarUrl(gravatarUrl: string): any;

src/components/avatar/index.tsx

Lines changed: 76 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {
1111
TextStyle,
1212
AccessibilityProps
1313
} from 'react-native';
14+
import memoize from 'memoize-one';
1415
import {Colors} from '../../style';
1516
import {forwardRef, asBaseComponent} from '../../commons/new';
1617
import {extractAccessibilityProps} from '../../commons/modifiers';
@@ -21,6 +22,7 @@ import Text from '../text';
2122
import Image, {ImageProps} from '../image';
2223
// @ts-ignore
2324
import AnimatedImage from '../animatedImage';
25+
import * as AvatarHelper from '../../helpers/AvatarHelper';
2426

2527
const deprecatedProps = [
2628
{old: 'isOnline', new: 'badgeProps.backgroundColor'},
@@ -44,6 +46,21 @@ export enum BadgePosition {
4446

4547
const DEFAULT_BADGE_SIZE = 'pimpleBig';
4648

49+
export type AutoColorsProps = {
50+
/**
51+
* Avatar colors to be used when useAutoColors is true
52+
*/
53+
avatarColors?: string[];
54+
/**
55+
* Replace the default hashing function (name -> number)
56+
*/
57+
hashFunction?: (name?: string) => number;
58+
/**
59+
* Background color in cases where the getBackgroundColor returns undefined.
60+
*/
61+
defaultColor?: string;
62+
};
63+
4764
export type AvatarProps = Pick<AccessibilityProps, 'accessibilityLabel'> & {
4865
/**
4966
* Adds fade in animation when Avatar image loads
@@ -93,6 +110,21 @@ export type AvatarProps = Pick<AccessibilityProps, 'accessibilityLabel'> & {
93110
* fails (equiv. to Image.onError()).
94111
*/
95112
onImageLoadError?: ImagePropsBase['onError'];
113+
/**
114+
* The name of the avatar user.
115+
* If no label is provided, the initials will be generated from the name.
116+
* autoColorsConfig will use the name to create the background color of the Avatar.
117+
*/
118+
name?: string;
119+
/**
120+
* Hash the name (or label) to get a color, so each name will have a specific color.
121+
* Default is false.
122+
*/
123+
useAutoColors?: boolean;
124+
/**
125+
* Send this to use the name to infer a backgroundColor
126+
*/
127+
autoColorsConfig?: AutoColorsProps;
96128
/**
97129
* Label that can represent initials
98130
*/
@@ -169,7 +201,6 @@ class Avatar extends PureComponent<AvatarProps> {
169201

170202
static defaultProps = {
171203
animate: false,
172-
backgroundColor: Colors.dark80,
173204
size: 50,
174205
labelColor: Colors.dark10,
175206
badgePosition: BadgePosition.TOP_RIGHT
@@ -318,14 +349,48 @@ class Avatar extends PureComponent<AvatarProps> {
318349
}
319350
}
320351

352+
getText = memoize((label, name) => {
353+
let text = label;
354+
if (_.isNil(label) && !_.isNil(name)) {
355+
text = AvatarHelper.getInitials(name);
356+
}
357+
358+
return text;
359+
});
360+
361+
get text() {
362+
const {label, name} = this.props;
363+
return this.getText(label, name);
364+
}
365+
366+
getBackgroundColor = memoize((text, avatarColors, hashFunction, defaultColor) => {
367+
return AvatarHelper.getBackgroundColor(text, avatarColors, hashFunction, defaultColor);
368+
});
369+
370+
get backgroundColor() {
371+
const {backgroundColor, useAutoColors, autoColorsConfig, name} = this.props;
372+
if (backgroundColor) {
373+
return backgroundColor;
374+
}
375+
376+
const {
377+
avatarColors = AvatarHelper.getAvatarColors(),
378+
hashFunction = AvatarHelper.hashStringToNumber,
379+
defaultColor = Colors.grey80
380+
} = autoColorsConfig || {};
381+
if (useAutoColors) {
382+
return this.getBackgroundColor(name, avatarColors, hashFunction, defaultColor);
383+
} else {
384+
return defaultColor;
385+
}
386+
}
387+
321388
render() {
322389
const {
323-
label,
324390
labelColor: color,
325391
source,
326392
//@ts-ignore
327393
imageSource,
328-
backgroundColor,
329394
onPress,
330395
containerStyle,
331396
children,
@@ -338,6 +403,7 @@ class Avatar extends PureComponent<AvatarProps> {
338403
const hasImage = !_.isUndefined(imageSource) || !_.isUndefined(source);
339404
const fontSizeToImageSizeRatio = 0.32;
340405
const fontSize = size * fontSizeToImageSizeRatio;
406+
const text = this.text;
341407

342408
return (
343409
<Container
@@ -351,11 +417,15 @@ class Avatar extends PureComponent<AvatarProps> {
351417
{...extractAccessibilityProps(this.props)}
352418
>
353419
<View
354-
style={[this.getInitialsContainer(), {backgroundColor}, hasImage && this.styles.initialsContainerWithInset]}
420+
style={[
421+
this.getInitialsContainer(),
422+
{backgroundColor: this.backgroundColor},
423+
hasImage && this.styles.initialsContainerWithInset
424+
]}
355425
>
356-
{!_.isUndefined(label) && (
426+
{!_.isUndefined(text) && (
357427
<Text numberOfLines={1} style={[{fontSize}, this.styles.initials, {color}]} testID={`${testID}.label`}>
358-
{label}
428+
{text}
359429
</Text>
360430
)}
361431
</View>

src/helpers/AvatarHelper.ts

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import _ from 'lodash';
33
import URL from 'url-parse';
44
import Colors from '../style/colors';
55

6-
function hashStringToNumber(str: string) {
6+
export function hashStringToNumber(str: string) {
77
let hash = 5381;
88
for (let i = 0; i < str.length; i++) {
99
const char = str.charCodeAt(i);
@@ -27,13 +27,13 @@ export function getColorById(id: string, avatarColors = getAvatarColors()) {
2727
return avatarColors[colorIndex];
2828
}
2929

30-
export function getInitials(name: string) {
30+
export function getInitials(name?: string, limit = 2) {
3131
let initials = '';
3232
if (name && _.isString(name)) {
3333
const nameSplitted = _.chain(name)
3434
.split(/\s+/g)
3535
.filter(word => word.length > 0)
36-
.take(2)
36+
.take(limit)
3737
.value();
3838
_.each(nameSplitted, (str) => {
3939
initials += str[0];
@@ -43,6 +43,19 @@ export function getInitials(name: string) {
4343
return _.toUpper(initials);
4444
}
4545

46+
export function getBackgroundColor(name?: string,
47+
avatarColors?: string[],
48+
hashFunction?: (name?: string) => number,
49+
defaultColor?: string) {
50+
if (!name || !avatarColors || !hashFunction) {
51+
return defaultColor;
52+
}
53+
54+
const hash = hashFunction(name);
55+
const index = Math.abs(hash % avatarColors.length);
56+
return avatarColors[index];
57+
}
58+
4659
export function isGravatarUrl(url: string) {
4760
const {hostname, pathname} = new URL(url);
4861
return _.split(hostname, '.').includes('gravatar') && pathname.startsWith('/avatar/');

src/helpers/__tests__/AvatarHelper.spec.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,14 +44,41 @@ describe('services/AvatarService', () => {
4444

4545
it('should getInitials', () => {
4646
expect(uut.getInitials('Austin Guerrero')).toBe('AG');
47+
expect(uut.getInitials('Austin Guerrero', 1)).toBe('A');
48+
expect(uut.getInitials('Austin Guerrero', 2)).toBe('AG');
49+
expect(uut.getInitials('Austin Guerrero', 3)).toBe('AG');
4750
expect(uut.getInitials('Austin Guerrero')).toBe('AG');
4851
expect(uut.getInitials('theresa simpson')).toBe('TS');
52+
expect(uut.getInitials('theresa simpson', 1)).toBe('T');
53+
expect(uut.getInitials('theresa simpson', 2)).toBe('TS');
54+
expect(uut.getInitials('theresa simpson', 3)).toBe('TS');
4955
expect(uut.getInitials('Sarah Michelle Galler')).toBe('SM');
56+
expect(uut.getInitials('Sarah Michelle Galler', 1)).toBe('S');
57+
expect(uut.getInitials('Sarah Michelle Galler', 2)).toBe('SM');
58+
expect(uut.getInitials('Sarah Michelle Galler', 3)).toBe('SMG');
5059
expect(uut.getInitials('Keith')).toBe('K');
5160
expect(uut.getInitials()).toBe('');
5261
expect(uut.getInitials(' Austin ')).toBe('A');
5362
});
5463

64+
it('should getBackgroundColor', () => {
65+
const avatarColors = uut.getAvatarColors();
66+
const hashFunction = uut.hashStringToNumber;
67+
const defaultColor = Colors.dark80;
68+
expect(uut.getBackgroundColor('', avatarColors, hashFunction, defaultColor)).toBe(defaultColor);
69+
expect(uut.getBackgroundColor(undefined, avatarColors, hashFunction, defaultColor)).toBe(defaultColor);
70+
expect(uut.getBackgroundColor(null, avatarColors, hashFunction, defaultColor)).toBe(defaultColor);
71+
expect(uut.getBackgroundColor('Austin Guerrero', undefined, hashFunction, defaultColor)).toBe(defaultColor);
72+
expect(uut.getBackgroundColor('Austin Guerrero', null, hashFunction, defaultColor)).toBe(defaultColor);
73+
expect(uut.getBackgroundColor('Austin Guerrero', avatarColors, undefined, defaultColor)).toBe(defaultColor);
74+
expect(uut.getBackgroundColor('Austin Guerrero', avatarColors, null, defaultColor)).toBe(defaultColor);
75+
expect(uut.getBackgroundColor('Austin Guerrero', avatarColors, hashFunction, defaultColor)).toBe(Colors.orange20);
76+
expect(uut.getBackgroundColor('theresa simpson', avatarColors, hashFunction, defaultColor)).toBe(Colors.green20);
77+
expect(uut.getBackgroundColor('Sarah Michelle Galler', avatarColors, hashFunction, defaultColor)).toBe(Colors.green20);
78+
expect(uut.getBackgroundColor('Keith', avatarColors, hashFunction, defaultColor)).toBe(Colors.green20);
79+
expect(uut.getBackgroundColor(' Austin ', avatarColors, hashFunction, defaultColor)).toBe(Colors.cyan20);
80+
});
81+
5582
describe('Is-gravatar query function', () => {
5683
it('should return true for a valid (known) gravatar url', () => {
5784
expect(uut.isGravatarUrl('https://www.gravatar.com/avatar/00000000000000000000000000000000')).toEqual(true);

0 commit comments

Comments
 (0)