-
Notifications
You must be signed in to change notification settings - Fork 298
Expand file tree
/
Copy pathAvatar.tsx
More file actions
112 lines (97 loc) · 2.97 KB
/
Avatar.tsx
File metadata and controls
112 lines (97 loc) · 2.97 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
import clsx from 'clsx';
import React, {
type ComponentPropsWithoutRef,
useEffect,
useMemo,
useState,
} from 'react';
import { IconUser } from '../Icons';
export type AvatarProps = {
/** URL of the avatar image */
imageUrl?: string;
/** Name of the user, used for avatar image alt text and title fallback */
userName?: string;
/** Online status indicator, not rendered if not of type boolean */
isOnline?: boolean;
size: '2xl' | 'xl' | 'lg' | 'md' | 'sm' | 'xs' | (string & {}) | null;
} & ComponentPropsWithoutRef<'div'>;
const getInitials = (name?: string) => {
const regex = /(\p{L}{1})\p{L}+/gu;
if (!name || name.trim().length === 0) {
return '';
}
const initials = Array.from(name?.matchAll(regex) || []);
if (!initials.length) {
return '';
}
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const startInitial = initials.at(0)![1];
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const endInitial = initials.length > 1 ? initials.at(-1)![1] : '';
return `${startInitial}${endInitial}`;
};
/**
* A round avatar image with fallback to username's first letter
*/
export const Avatar = ({
className,
imageUrl,
isOnline,
size,
userName,
...rest
}: AvatarProps) => {
const [error, setError] = useState(false);
useEffect(() => () => setError(false), [imageUrl]);
const nameString = userName?.toString() || '';
const avatarImageAlt = nameString.trim();
const sizeAwareInitials = useMemo(() => {
const initials = getInitials(nameString);
if (size === 'sm' || size === 'xs') {
return initials.charAt(0);
}
return initials;
}, [nameString, size]);
const showImage = typeof imageUrl === 'string' && imageUrl && !error;
return (
<div
className={clsx(`str-chat__avatar`, className, {
'str-chat__avatar--multiple-letters': sizeAwareInitials.length > 1,
'str-chat__avatar--no-letters': !sizeAwareInitials.length,
'str-chat__avatar--one-letter': sizeAwareInitials.length === 1,
[`str-chat__avatar--size-${size}`]: typeof size === 'string',
})}
data-testid='avatar'
role='button'
title={userName}
{...rest}
>
{typeof isOnline === 'boolean' && (
<div
className={clsx('str-chat__avatar-status-badge', {
'str-chat__avatar-status-badge--offline': !isOnline,
'str-chat__avatar-status-badge--online': isOnline,
})}
/>
)}
{showImage ? (
<img
alt={avatarImageAlt}
className='str-chat__avatar-image'
data-testid='avatar-img'
onError={() => setError(true)}
src={imageUrl}
/>
) : (
<>
{!!sizeAwareInitials.length && (
<div className='str-chat__avatar-initials' data-testid='avatar-fallback'>
{sizeAwareInitials}
</div>
)}
{!sizeAwareInitials.length && <IconUser />}
</>
)}
</div>
);
};