| 
									
										
										
										
											2021-01-14 12:07:05 -06:00
										 |  |  | // Copyright 2018-2021 Signal Messenger, LLC
 | 
					
						
							| 
									
										
										
										
											2020-10-30 15:34:04 -05:00
										 |  |  | // SPDX-License-Identifier: AGPL-3.0-only
 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  | import React, { | 
					
						
							|  |  |  |   FunctionComponent, | 
					
						
							|  |  |  |   ReactNode, | 
					
						
							|  |  |  |   useEffect, | 
					
						
							|  |  |  |   useState, | 
					
						
							|  |  |  | } from 'react'; | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  | import classNames from 'classnames'; | 
					
						
							| 
									
										
										
										
											2021-04-27 08:20:17 -05:00
										 |  |  | import { noop } from 'lodash'; | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-29 14:16:48 -08:00
										 |  |  | import { Spinner } from './Spinner'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  | import { getInitials } from '../util/getInitials'; | 
					
						
							| 
									
										
										
										
											2020-08-13 13:53:45 -07:00
										 |  |  | import { LocalizerType } from '../types/Util'; | 
					
						
							|  |  |  | import { ColorType } from '../types/Colors'; | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  | import * as log from '../logging/log'; | 
					
						
							| 
									
										
										
										
											2021-04-27 08:20:17 -05:00
										 |  |  | import { assert } from '../util/assert'; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | export enum AvatarBlur { | 
					
						
							|  |  |  |   NoBlur, | 
					
						
							|  |  |  |   BlurPicture, | 
					
						
							|  |  |  |   BlurPictureWithClickToView, | 
					
						
							|  |  |  | } | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-11-19 13:13:36 -05:00
										 |  |  | export enum AvatarSize { | 
					
						
							|  |  |  |   TWENTY_EIGHT = 28, | 
					
						
							|  |  |  |   THIRTY_TWO = 32, | 
					
						
							|  |  |  |   FIFTY_TWO = 52, | 
					
						
							|  |  |  |   EIGHTY = 80, | 
					
						
							|  |  |  |   NINETY_SIX = 96, | 
					
						
							|  |  |  |   ONE_HUNDRED_TWELVE = 112, | 
					
						
							|  |  |  | } | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-05-27 17:37:06 -04:00
										 |  |  | export type Props = { | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  |   avatarPath?: string; | 
					
						
							| 
									
										
										
										
											2021-04-27 08:20:17 -05:00
										 |  |  |   blur?: AvatarBlur; | 
					
						
							| 
									
										
										
										
											2020-03-26 14:47:35 -07:00
										 |  |  |   color?: ColorType; | 
					
						
							| 
									
										
										
										
											2021-01-29 14:16:48 -08:00
										 |  |  |   loading?: boolean; | 
					
						
							| 
									
										
										
										
											2020-03-26 14:47:35 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  |   conversationType: 'group' | 'direct'; | 
					
						
							| 
									
										
										
										
											2019-01-30 17:45:58 -08:00
										 |  |  |   noteToSelf?: boolean; | 
					
						
							| 
									
										
										
										
											2020-07-23 18:35:32 -07:00
										 |  |  |   title: string; | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  |   name?: string; | 
					
						
							|  |  |  |   phoneNumber?: string; | 
					
						
							|  |  |  |   profileName?: string; | 
					
						
							| 
									
										
										
										
											2020-11-19 13:13:36 -05:00
										 |  |  |   size: AvatarSize; | 
					
						
							| 
									
										
										
										
											2019-10-17 11:22:07 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   onClick?: () => unknown; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   // Matches Popper's RefHandler type
 | 
					
						
							| 
									
										
										
										
											2020-01-06 20:49:00 -05:00
										 |  |  |   innerRef?: React.Ref<HTMLDivElement>; | 
					
						
							| 
									
										
										
										
											2019-10-17 11:22:07 -07:00
										 |  |  | 
 | 
					
						
							|  |  |  |   i18n: LocalizerType; | 
					
						
							| 
									
										
										
										
											2020-05-27 17:37:06 -04:00
										 |  |  | } & Pick<React.HTMLProps<HTMLDivElement>, 'className'>; | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  | export const Avatar: FunctionComponent<Props> = ({ | 
					
						
							|  |  |  |   avatarPath, | 
					
						
							|  |  |  |   className, | 
					
						
							|  |  |  |   color, | 
					
						
							|  |  |  |   conversationType, | 
					
						
							|  |  |  |   i18n, | 
					
						
							|  |  |  |   innerRef, | 
					
						
							|  |  |  |   loading, | 
					
						
							|  |  |  |   noteToSelf, | 
					
						
							|  |  |  |   onClick, | 
					
						
							|  |  |  |   size, | 
					
						
							|  |  |  |   title, | 
					
						
							| 
									
										
										
										
											2021-04-27 08:20:17 -05:00
										 |  |  |   blur = AvatarBlur.NoBlur, | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  | }) => { | 
					
						
							|  |  |  |   const [imageBroken, setImageBroken] = useState(false); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     setImageBroken(false); | 
					
						
							|  |  |  |   }, [avatarPath]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-27 08:20:17 -05:00
										 |  |  |   useEffect(() => { | 
					
						
							|  |  |  |     if (!avatarPath) { | 
					
						
							|  |  |  |       return noop; | 
					
						
							|  |  |  |     } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const image = new Image(); | 
					
						
							|  |  |  |     image.src = avatarPath; | 
					
						
							|  |  |  |     image.onerror = () => { | 
					
						
							|  |  |  |       log.warn('Avatar: Image failed to load; failing over to placeholder'); | 
					
						
							|  |  |  |       setImageBroken(true); | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     return () => { | 
					
						
							|  |  |  |       image.onerror = noop; | 
					
						
							|  |  |  |     }; | 
					
						
							|  |  |  |   }, [avatarPath]); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |   const initials = getInitials(title); | 
					
						
							|  |  |  |   const hasImage = !noteToSelf && avatarPath && !imageBroken; | 
					
						
							|  |  |  |   const shouldUseInitials = | 
					
						
							|  |  |  |     !hasImage && conversationType === 'direct' && Boolean(initials); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   let contents: ReactNode; | 
					
						
							|  |  |  |   if (loading) { | 
					
						
							|  |  |  |     const svgSize = size < 40 ? 'small' : 'normal'; | 
					
						
							|  |  |  |     contents = ( | 
					
						
							|  |  |  |       <div className="module-Avatar__spinner-container"> | 
					
						
							|  |  |  |         <Spinner | 
					
						
							|  |  |  |           size={`${size - 8}px`} | 
					
						
							|  |  |  |           svgSize={svgSize} | 
					
						
							|  |  |  |           direction="on-avatar" | 
					
						
							|  |  |  |         /> | 
					
						
							|  |  |  |       </div> | 
					
						
							| 
									
										
										
										
											2020-09-11 17:46:52 -07:00
										 |  |  |     ); | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |   } else if (hasImage) { | 
					
						
							| 
									
										
										
										
											2021-04-27 08:20:17 -05:00
										 |  |  |     assert(avatarPath, 'avatarPath should be defined here'); | 
					
						
							|  |  |  |     assert( | 
					
						
							|  |  |  |       blur !== AvatarBlur.BlurPictureWithClickToView || size >= 100, | 
					
						
							|  |  |  |       'Rendering "click to view" for a small avatar. This may not render correctly' | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     const isBlurred = | 
					
						
							|  |  |  |       blur === AvatarBlur.BlurPicture || | 
					
						
							|  |  |  |       blur === AvatarBlur.BlurPictureWithClickToView; | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |     contents = ( | 
					
						
							| 
									
										
										
										
											2021-04-27 08:20:17 -05:00
										 |  |  |       <> | 
					
						
							|  |  |  |         <div | 
					
						
							|  |  |  |           className="module-Avatar__image" | 
					
						
							|  |  |  |           style={{ | 
					
						
							|  |  |  |             backgroundImage: `url('${encodeURI(avatarPath)}')`, | 
					
						
							|  |  |  |             ...(isBlurred ? { filter: `blur(${Math.ceil(size / 2)}px)` } : {}), | 
					
						
							|  |  |  |           }} | 
					
						
							|  |  |  |         /> | 
					
						
							|  |  |  |         {blur === AvatarBlur.BlurPictureWithClickToView && ( | 
					
						
							|  |  |  |           <div className="module-Avatar__click-to-view">{i18n('view')}</div> | 
					
						
							|  |  |  |         )} | 
					
						
							|  |  |  |       </> | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  |     ); | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |   } else if (noteToSelf) { | 
					
						
							|  |  |  |     contents = ( | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  |       <div | 
					
						
							|  |  |  |         className={classNames( | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |           'module-Avatar__icon', | 
					
						
							|  |  |  |           'module-Avatar__icon--note-to-self' | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  |         )} | 
					
						
							|  |  |  |       /> | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |   } else if (shouldUseInitials) { | 
					
						
							|  |  |  |     contents = ( | 
					
						
							|  |  |  |       <div | 
					
						
							| 
									
										
										
										
											2021-04-27 08:20:17 -05:00
										 |  |  |         aria-hidden="true" | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |         className="module-Avatar__label" | 
					
						
							|  |  |  |         style={{ fontSize: Math.ceil(size * 0.5) }} | 
					
						
							|  |  |  |       > | 
					
						
							|  |  |  |         {initials} | 
					
						
							| 
									
										
										
										
											2021-01-29 14:16:48 -08:00
										 |  |  |       </div> | 
					
						
							|  |  |  |     ); | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |   } else { | 
					
						
							|  |  |  |     contents = ( | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  |       <div | 
					
						
							|  |  |  |         className={classNames( | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |           'module-Avatar__icon', | 
					
						
							|  |  |  |           `module-Avatar__icon--${conversationType}` | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  |         )} | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |       /> | 
					
						
							|  |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |   if (onClick) { | 
					
						
							|  |  |  |     contents = ( | 
					
						
							|  |  |  |       <button className="module-Avatar__button" type="button" onClick={onClick}> | 
					
						
							| 
									
										
										
										
											2019-11-07 13:36:16 -08:00
										 |  |  |         {contents} | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |       </button> | 
					
						
							| 
									
										
										
										
											2018-09-26 17:23:17 -07:00
										 |  |  |     ); | 
					
						
							|  |  |  |   } | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  |   return ( | 
					
						
							|  |  |  |     <div | 
					
						
							| 
									
										
										
										
											2021-04-27 08:20:17 -05:00
										 |  |  |       aria-label={i18n('contactAvatarAlt', [title])} | 
					
						
							| 
									
										
										
										
											2021-04-21 11:32:23 -05:00
										 |  |  |       className={classNames( | 
					
						
							|  |  |  |         'module-Avatar', | 
					
						
							|  |  |  |         hasImage ? 'module-Avatar--with-image' : 'module-Avatar--no-image', | 
					
						
							|  |  |  |         { | 
					
						
							|  |  |  |           [`module-Avatar--${color}`]: !hasImage, | 
					
						
							|  |  |  |         }, | 
					
						
							|  |  |  |         className | 
					
						
							|  |  |  |       )} | 
					
						
							|  |  |  |       style={{ | 
					
						
							|  |  |  |         minWidth: size, | 
					
						
							|  |  |  |         width: size, | 
					
						
							|  |  |  |         height: size, | 
					
						
							|  |  |  |       }} | 
					
						
							|  |  |  |       ref={innerRef} | 
					
						
							|  |  |  |     > | 
					
						
							|  |  |  |       {contents} | 
					
						
							|  |  |  |     </div> | 
					
						
							|  |  |  |   ); | 
					
						
							|  |  |  | }; |