import React, { useContext, useMemo, useState } from "react"; import md5 from "md5"; import classNames from "classnames"; import { bemHOF } from "../../utilities/bem"; import { colors } from "../../tokens"; import { Box, BoxProps } from "../Box"; import { Icon, ICON_TYPE } from "../Icon"; import { Flex } from "../Flex"; import { Text } from "../Text"; import { Image } from "../Image"; import { Tooltip } from "../Tooltip"; import { Svg, SvgProps } from "../Svg"; import { AvatarGroup } from "./AvatarGroup"; import { AvatarSizesForGravatar, AvatarSize, AVATAR_TYPE } from "./types"; import { AvatarContext } from "./context"; const cn = bemHOF("Avatar"); const SystemIcon = (props: SvgProps) => { return ( ); }; // Deprecated; Remove after dual-branding in Comply is no longer needed // Exported to create a dual-branded system avatar in Comply export const SystemIconConveyor = (props: SvgProps) => ( ); // Deprecated; Remove after dual-branding in Comply is no longer needed // Exported to create a dual-branded system avatar in Comply export const SystemIconAptible = (props: SvgProps) => ( ); export interface SharedAvatarProps extends BoxProps { /** * The size to render the avatar. */ size?: AvatarSize; /** * Makes the avatar a square with rounded corners and a box shadow; good to * use when overlapping avatars. */ square?: boolean; /** * Adds a border around the avatar. */ bordered?: boolean; /** * Alternate text for accessibility; most times it’s better to leave this * null. */ alt?: string; } type AnonymousAvatarProps = SharedAvatarProps & { type: AVATAR_TYPE.ANONYMOUS; }; type SystemAvatarProps = SharedAvatarProps & { type: AVATAR_TYPE.SYSTEM; }; interface UserAvatarComponentProps extends GravatarOrInitialsProps { /** * Path to an image. */ imgSrc?: string; /** * Whether or not to use a tooltip to display the user’s name; it * only applies when `type=user`. If `name` is not provided, it’ll * use `email` if it exists. The tooltip won’t show if either is * missing. */ enableTooltip?: boolean; } type UserAvatarProps = SharedAvatarProps & UserAvatarComponentProps & { type: AVATAR_TYPE.USER; }; export type AvatarProps = | AnonymousAvatarProps | SystemAvatarProps | UserAvatarProps; const avatarInitialBackgroundColors: string[] = [ colors.green["600"], colors.gold["600"], colors.orange["600"], colors.red["600"], colors.pink["600"], colors.purple["600"], colors.blue["600"], colors.cyan["600"], ]; interface InitialsProps { /** * Email address for the user; this is used to look up a Gravatar * image or as a key to provide a unique color when the initials fallback * is used. */ email: string; /** * The user’s name; this will be used to automatically create the * user’s initials. It’ll also display in a tooltip when * `enableTooltip=true`. */ name?: string; /** * The user’s first name initial; this can be omitted if a `name` is passed. */ firstInitial?: string; /** * The user’s last name initial; this can be omitted if a `name` is passed. */ lastInitial?: string; } interface GravatarOrInitialsProps extends InitialsProps { size?: AvatarSize; alt?: string; } const Initials = ({ email, name, firstInitial, lastInitial, }: InitialsProps) => { const initialsBgColor: string = useMemo(() => { const fallbackColor = colors.gray["600"]; if (!email) return fallbackColor; let hash = 0; for (let i = 0; i < email.length; i += 1) { // eslint-disable-next-line no-bitwise hash = email.charCodeAt(i) + ((hash << 5) - hash); } const colorIndex = Math.abs(hash % avatarInitialBackgroundColors.length); return avatarInitialBackgroundColors[colorIndex] || fallbackColor; }, [email]); // Generate the initials from the name const initialsFromName: string = name ? name .split(" ") // Separate first and last names .map((n: string) => n.charAt(0)) // Get the first letter of each name .join("") // Join them back together .slice(0, 2) // Take the first two initials (in case of multiple word surnames) : ""; // Use the first letter of the email address instead of initials if not provided const initials = firstInitial === undefined && lastInitial === undefined ? email.charAt(0) : `${firstInitial}${lastInitial}`; return ( {initialsFromName || initials} ); }; const GravatarOrInitials = ({ email, size = "medium", name, firstInitial, lastInitial, alt, }: GravatarOrInitialsProps) => { const [hasImageError, setImageError] = useState(false); const src = useMemo(() => { const md5Email = email ? md5(email.trim().toLowerCase(), { encoding: "binary" }) : null; return `https://www.gravatar.com/avatar/${md5Email}?d=404&r=pg&s=${AvatarSizesForGravatar[size]}`; }, [email, size]); if (hasImageError) { return ( ); } return ( {alt} setImageError(true)} onLoad={() => setImageError(false)} /> ); }; const UserAvatar = ({ imgSrc, email = "", alt, ...rest }: UserAvatarComponentProps) => { // If an imgSrc is provided, just use that as the avatar if (imgSrc) { return {alt}; } // If no imgSrc, then use the Gravatar return ; }; const AnonymousAvatar = () => ( ); const SystemAvatar = () => { return ( ); }; // Deprecated; Remove after dual-branding in Comply is no longer needed // Exported to create a dual-branded system avatar in Comply export const SystemAvatarConveyor = () => { return ; }; // Deprecated; Remove after dual-branding in Comply is no longer needed // Exported to create a dual-branded system avatar in Comply export const SystemAvatarAptible = () => { return ( ); }; const NonUserAvatarComponent = (type: AVATAR_TYPE) => { return { anonymous: , user: , system: , }[type]; }; export const AvatarWrapper = ({ children, size, square, bordered, className, ...rest }: SharedAvatarProps) => ( {children} ); export const Avatar = ({ size: sizeProp = "medium", square, bordered, className, style, alt, ...rest }: AvatarProps) => { const avatarContext = useContext(AvatarContext); const size = avatarContext ? avatarContext.size : sizeProp; if (rest.type === AVATAR_TYPE.USER && (rest.email || rest.imgSrc)) { const { type, email, imgSrc, name, firstInitial, lastInitial, enableTooltip = false, ...userRest } = rest; return ( {enableTooltip && (name || email) ? ( } > {name || email} ) : ( )} ); } const { type, ...nonUserRest } = rest; if (type === AVATAR_TYPE.USER) { if ("name" in nonUserRest) { delete nonUserRest.name; } if ("firstInitial" in nonUserRest) { delete nonUserRest.firstInitial; } if ("lastInitial" in nonUserRest) { delete nonUserRest.lastInitial; } } return ( {NonUserAvatarComponent(type)} ); }; Avatar.Group = AvatarGroup;