Skip to content

Avatar - add name with initials and backgroundColor specific to that name #1239

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Apr 8, 2021
Merged
49 changes: 48 additions & 1 deletion generatedTypes/components/avatar/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,20 @@ export declare enum BadgePosition {
BOTTOM_RIGHT = "BOTTOM_RIGHT",
BOTTOM_LEFT = "BOTTOM_LEFT"
}
export declare type AutoColorsProps = {
/**
* Avatar colors to be used when useAutoColors is true
*/
avatarColors?: string[];
/**
* Replace the default hashing function (name -> number)
*/
hashFunction?: (name?: string) => number;
/**
* Background color in cases where the getBackgroundColor returns undefined.
*/
defaultColor?: string;
};
export declare type AvatarProps = Pick<AccessibilityProps, 'accessibilityLabel'> & {
/**
* Adds fade in animation when Avatar image loads
Expand Down Expand Up @@ -63,6 +77,21 @@ export declare type AvatarProps = Pick<AccessibilityProps, 'accessibilityLabel'>
* fails (equiv. to Image.onError()).
*/
onImageLoadError?: ImagePropsBase['onError'];
/**
* The name of the avatar user.
* If no label is provided, the initials will be generated from the name.
* autoColorsConfig will use the name to create the background color of the Avatar.
*/
name?: string;
/**
* Hash the name (or label) to get a color, so each name will have a specific color.
* Default is false.
*/
useAutoColors?: boolean;
/**
* Send this to use the name to infer a backgroundColor
*/
autoColorsConfig?: AutoColorsProps;
/**
* Label that can represent initials
*/
Expand Down Expand Up @@ -125,7 +154,6 @@ declare class Avatar extends PureComponent<AvatarProps> {
static badgePosition: typeof BadgePosition;
static defaultProps: {
animate: boolean;
backgroundColor: string;
size: number;
labelColor: string;
badgePosition: BadgePosition;
Expand All @@ -141,6 +169,10 @@ declare class Avatar extends PureComponent<AvatarProps> {
renderBadge(): JSX.Element | undefined;
renderRibbon(): JSX.Element | undefined;
renderImage(): JSX.Element | undefined;
getText: (this: any, label: any, name: any) => any;
get text(): any;
getBackgroundColor: (this: any, text: any, avatarColors: any, hashFunction: any, defaultColor: any) => string | undefined;
get backgroundColor(): string | undefined;
render(): JSX.Element;
}
declare function createStyles(props: AvatarProps): {
Expand Down Expand Up @@ -211,6 +243,21 @@ declare const _default: React.ComponentClass<Pick<AccessibilityProps, "accessibi
* fails (equiv. to Image.onError()).
*/
onImageLoadError?: ((error: import("react-native").NativeSyntheticEvent<import("react-native").ImageErrorEventData>) => void) | undefined;
/**
* The name of the avatar user.
* If no label is provided, the initials will be generated from the name.
* autoColorsConfig will use the name to create the background color of the Avatar.
*/
name?: string | undefined;
/**
* Hash the name (or label) to get a color, so each name will have a specific color.
* Default is false.
*/
useAutoColors?: boolean | undefined;
/**
* Send this to use the name to infer a backgroundColor
*/
autoColorsConfig?: AutoColorsProps | undefined;
/**
* Label that can represent initials
*/
Expand Down
4 changes: 3 additions & 1 deletion generatedTypes/helpers/AvatarHelper.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
export declare function hashStringToNumber(str: string): number;
export declare function getAvatarColors(): string[];
export declare function getColorById(id: string, avatarColors?: string[]): string;
export declare function getInitials(name: string): string;
export declare function getInitials(name?: string, limit?: number): string;
export declare function getBackgroundColor(name?: string, avatarColors?: string[], hashFunction?: (name?: string) => number, defaultColor?: string): string | undefined;
export declare function isGravatarUrl(url: string): any;
export declare function isBlankGravatarUrl(url: string): boolean;
export declare function patchGravatarUrl(gravatarUrl: string): any;
82 changes: 76 additions & 6 deletions src/components/avatar/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
TextStyle,
AccessibilityProps
} from 'react-native';
import memoize from 'memoize-one';
import {Colors} from '../../style';
import {forwardRef, asBaseComponent} from '../../commons/new';
import {extractAccessibilityProps} from '../../commons/modifiers';
Expand All @@ -21,6 +22,7 @@ import Text from '../text';
import Image, {ImageProps} from '../image';
// @ts-ignore
import AnimatedImage from '../animatedImage';
import * as AvatarHelper from '../../helpers/AvatarHelper';

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

const DEFAULT_BADGE_SIZE = 'pimpleBig';

export type AutoColorsProps = {
/**
* Avatar colors to be used when useAutoColors is true
*/
avatarColors?: string[];
/**
* Replace the default hashing function (name -> number)
*/
hashFunction?: (name?: string) => number;
/**
* Background color in cases where the getBackgroundColor returns undefined.
*/
defaultColor?: string;
};

export type AvatarProps = Pick<AccessibilityProps, 'accessibilityLabel'> & {
/**
* Adds fade in animation when Avatar image loads
Expand Down Expand Up @@ -93,6 +110,21 @@ export type AvatarProps = Pick<AccessibilityProps, 'accessibilityLabel'> & {
* fails (equiv. to Image.onError()).
*/
onImageLoadError?: ImagePropsBase['onError'];
/**
* The name of the avatar user.
* If no label is provided, the initials will be generated from the name.
* autoColorsConfig will use the name to create the background color of the Avatar.
*/
name?: string;
/**
* Hash the name (or label) to get a color, so each name will have a specific color.
* Default is false.
*/
useAutoColors?: boolean;
/**
* Send this to use the name to infer a backgroundColor
*/
autoColorsConfig?: AutoColorsProps;
/**
* Label that can represent initials
*/
Expand Down Expand Up @@ -169,7 +201,6 @@ class Avatar extends PureComponent<AvatarProps> {

static defaultProps = {
animate: false,
backgroundColor: Colors.dark80,
size: 50,
labelColor: Colors.dark10,
badgePosition: BadgePosition.TOP_RIGHT
Expand Down Expand Up @@ -318,14 +349,48 @@ class Avatar extends PureComponent<AvatarProps> {
}
}

getText = memoize((label, name) => {
let text = label;
if (_.isNil(label) && !_.isNil(name)) {
text = AvatarHelper.getInitials(name);
}

return text;
});

get text() {
const {label, name} = this.props;
return this.getText(label, name);
}

getBackgroundColor = memoize((text, avatarColors, hashFunction, defaultColor) => {
return AvatarHelper.getBackgroundColor(text, avatarColors, hashFunction, defaultColor);
});

get backgroundColor() {
const {backgroundColor, useAutoColors, autoColorsConfig, name} = this.props;
if (backgroundColor) {
return backgroundColor;
}

const {
avatarColors = AvatarHelper.getAvatarColors(),
hashFunction = AvatarHelper.hashStringToNumber,
defaultColor = Colors.grey80
} = autoColorsConfig || {};
if (useAutoColors) {
return this.getBackgroundColor(name, avatarColors, hashFunction, defaultColor);
} else {
return defaultColor;
}
}

render() {
const {
label,
labelColor: color,
source,
//@ts-ignore
imageSource,
backgroundColor,
onPress,
containerStyle,
children,
Expand All @@ -338,6 +403,7 @@ class Avatar extends PureComponent<AvatarProps> {
const hasImage = !_.isUndefined(imageSource) || !_.isUndefined(source);
const fontSizeToImageSizeRatio = 0.32;
const fontSize = size * fontSizeToImageSizeRatio;
const text = this.text;

return (
<Container
Expand All @@ -351,11 +417,15 @@ class Avatar extends PureComponent<AvatarProps> {
{...extractAccessibilityProps(this.props)}
>
<View
style={[this.getInitialsContainer(), {backgroundColor}, hasImage && this.styles.initialsContainerWithInset]}
style={[
this.getInitialsContainer(),
{backgroundColor: this.backgroundColor},
hasImage && this.styles.initialsContainerWithInset
]}
>
{!_.isUndefined(label) && (
{!_.isUndefined(text) && (
<Text numberOfLines={1} style={[{fontSize}, this.styles.initials, {color}]} testID={`${testID}.label`}>
{label}
{text}
</Text>
)}
</View>
Expand Down
19 changes: 16 additions & 3 deletions src/helpers/AvatarHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import _ from 'lodash';
import URL from 'url-parse';
import Colors from '../style/colors';

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

export function getInitials(name: string) {
export function getInitials(name?: string, limit = 2) {
let initials = '';
if (name && _.isString(name)) {
const nameSplitted = _.chain(name)
.split(/\s+/g)
.filter(word => word.length > 0)
.take(2)
.take(limit)
.value();
_.each(nameSplitted, (str) => {
initials += str[0];
Expand All @@ -43,6 +43,19 @@ export function getInitials(name: string) {
return _.toUpper(initials);
}

export function getBackgroundColor(name?: string,
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

consider adding few simple tests to cover the new functionalities

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done (found a bug!)

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The power of testings (:

avatarColors?: string[],
hashFunction?: (name?: string) => number,
defaultColor?: string) {
if (!name || !avatarColors || !hashFunction) {
return defaultColor;
}

const hash = hashFunction(name);
const index = Math.abs(hash % avatarColors.length);
return avatarColors[index];
}

export function isGravatarUrl(url: string) {
const {hostname, pathname} = new URL(url);
return _.split(hostname, '.').includes('gravatar') && pathname.startsWith('/avatar/');
Expand Down
27 changes: 27 additions & 0 deletions src/helpers/__tests__/AvatarHelper.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,14 +44,41 @@ describe('services/AvatarService', () => {

it('should getInitials', () => {
expect(uut.getInitials('Austin Guerrero')).toBe('AG');
expect(uut.getInitials('Austin Guerrero', 1)).toBe('A');
expect(uut.getInitials('Austin Guerrero', 2)).toBe('AG');
expect(uut.getInitials('Austin Guerrero', 3)).toBe('AG');
expect(uut.getInitials('Austin Guerrero')).toBe('AG');
expect(uut.getInitials('theresa simpson')).toBe('TS');
expect(uut.getInitials('theresa simpson', 1)).toBe('T');
expect(uut.getInitials('theresa simpson', 2)).toBe('TS');
expect(uut.getInitials('theresa simpson', 3)).toBe('TS');
expect(uut.getInitials('Sarah Michelle Galler')).toBe('SM');
expect(uut.getInitials('Sarah Michelle Galler', 1)).toBe('S');
expect(uut.getInitials('Sarah Michelle Galler', 2)).toBe('SM');
expect(uut.getInitials('Sarah Michelle Galler', 3)).toBe('SMG');
expect(uut.getInitials('Keith')).toBe('K');
expect(uut.getInitials()).toBe('');
expect(uut.getInitials(' Austin ')).toBe('A');
});

it('should getBackgroundColor', () => {
const avatarColors = uut.getAvatarColors();
const hashFunction = uut.hashStringToNumber;
const defaultColor = Colors.dark80;
expect(uut.getBackgroundColor('', avatarColors, hashFunction, defaultColor)).toBe(defaultColor);
expect(uut.getBackgroundColor(undefined, avatarColors, hashFunction, defaultColor)).toBe(defaultColor);
expect(uut.getBackgroundColor(null, avatarColors, hashFunction, defaultColor)).toBe(defaultColor);
expect(uut.getBackgroundColor('Austin Guerrero', undefined, hashFunction, defaultColor)).toBe(defaultColor);
expect(uut.getBackgroundColor('Austin Guerrero', null, hashFunction, defaultColor)).toBe(defaultColor);
expect(uut.getBackgroundColor('Austin Guerrero', avatarColors, undefined, defaultColor)).toBe(defaultColor);
expect(uut.getBackgroundColor('Austin Guerrero', avatarColors, null, defaultColor)).toBe(defaultColor);
expect(uut.getBackgroundColor('Austin Guerrero', avatarColors, hashFunction, defaultColor)).toBe(Colors.orange20);
expect(uut.getBackgroundColor('theresa simpson', avatarColors, hashFunction, defaultColor)).toBe(Colors.green20);
expect(uut.getBackgroundColor('Sarah Michelle Galler', avatarColors, hashFunction, defaultColor)).toBe(Colors.green20);
expect(uut.getBackgroundColor('Keith', avatarColors, hashFunction, defaultColor)).toBe(Colors.green20);
expect(uut.getBackgroundColor(' Austin ', avatarColors, hashFunction, defaultColor)).toBe(Colors.cyan20);
});

describe('Is-gravatar query function', () => {
it('should return true for a valid (known) gravatar url', () => {
expect(uut.isGravatarUrl('https://www.gravatar.com/avatar/00000000000000000000000000000000')).toEqual(true);
Expand Down