You cannot select more than 25 topics
			Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
		
		
		
		
		
			
		
			
				
	
	
		
			216 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			216 lines
		
	
	
		
			5.0 KiB
		
	
	
	
		
			TypeScript
		
	
| import React from 'react';
 | |
| import classNames from 'classnames';
 | |
| 
 | |
| import { JazzIcon } from './JazzIcon';
 | |
| import { getInitials } from '../util/getInitials';
 | |
| import { LocalizerType } from '../types/Util';
 | |
| 
 | |
| interface Props {
 | |
|   avatarPath?: string;
 | |
|   color?: string;
 | |
|   conversationType: 'group' | 'direct';
 | |
|   noteToSelf?: boolean;
 | |
|   name?: string;
 | |
|   phoneNumber?: string;
 | |
|   profileName?: string;
 | |
|   size: number;
 | |
|   borderColor?: string;
 | |
|   borderWidth?: number;
 | |
|   i18n?: LocalizerType;
 | |
|   onAvatarClick?: () => void;
 | |
| }
 | |
| 
 | |
| interface State {
 | |
|   imageBroken: boolean;
 | |
| }
 | |
| 
 | |
| export class Avatar extends React.PureComponent<Props, State> {
 | |
|   public handleImageErrorBound: () => void;
 | |
|   public onAvatarClickBound: (e: any) => void;
 | |
| 
 | |
|   public constructor(props: Props) {
 | |
|     super(props);
 | |
| 
 | |
|     this.handleImageErrorBound = this.handleImageError.bind(this);
 | |
|     this.onAvatarClickBound = this.onAvatarClick.bind(this);
 | |
| 
 | |
|     this.state = {
 | |
|       imageBroken: false,
 | |
|     };
 | |
|   }
 | |
| 
 | |
|   public handleImageError() {
 | |
|     // tslint:disable-next-line no-console
 | |
|     console.log('Avatar: Image failed to load; failing over to placeholder');
 | |
|     this.setState({
 | |
|       imageBroken: true,
 | |
|     });
 | |
|   }
 | |
| 
 | |
|   public renderIdenticon() {
 | |
|     const { phoneNumber, borderColor, borderWidth, size } = this.props;
 | |
| 
 | |
|     if (!phoneNumber) {
 | |
|       return this.renderNoImage();
 | |
|     }
 | |
| 
 | |
|     const borderStyle = this.getBorderStyle(borderColor, borderWidth);
 | |
| 
 | |
|     // Generate the seed
 | |
|     const hash = phoneNumber.substring(0, 12);
 | |
|     const seed = parseInt(hash, 16) || 1234;
 | |
| 
 | |
|     return <JazzIcon seed={seed} diameter={size} paperStyles={borderStyle} />;
 | |
|   }
 | |
| 
 | |
|   public renderImage() {
 | |
|     const {
 | |
|       avatarPath,
 | |
|       name,
 | |
|       phoneNumber,
 | |
|       profileName,
 | |
|       borderColor,
 | |
|       borderWidth,
 | |
|     } = this.props;
 | |
|     const { imageBroken } = this.state;
 | |
| 
 | |
|     if (!avatarPath || imageBroken) {
 | |
|       return null;
 | |
|     }
 | |
| 
 | |
|     const title = `${name || phoneNumber}${
 | |
|       !name && profileName ? ` ~${profileName}` : ''
 | |
|     }`;
 | |
| 
 | |
|     const borderStyle = this.getBorderStyle(borderColor, borderWidth);
 | |
| 
 | |
|     return (
 | |
|       <img
 | |
|         style={borderStyle}
 | |
|         onError={this.handleImageErrorBound}
 | |
|         alt={window.i18n('contactAvatarAlt', [title])}
 | |
|         src={avatarPath}
 | |
|       />
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   public renderNoImage() {
 | |
|     const {
 | |
|       conversationType,
 | |
|       name,
 | |
|       noteToSelf,
 | |
|       size,
 | |
|       borderColor,
 | |
|       borderWidth,
 | |
|     } = this.props;
 | |
| 
 | |
|     const initials = getInitials(name);
 | |
|     const isGroup = conversationType === 'group';
 | |
| 
 | |
|     if (noteToSelf) {
 | |
|       return (
 | |
|         <div
 | |
|           className={classNames(
 | |
|             'module-avatar__icon',
 | |
|             'module-avatar__icon--note-to-self',
 | |
|             `module-avatar__icon--${size}`
 | |
|           )}
 | |
|         />
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     const borderStyle = this.getBorderStyle(borderColor, borderWidth);
 | |
| 
 | |
|     if (!isGroup && initials) {
 | |
|       return (
 | |
|         <div
 | |
|           className={classNames(
 | |
|             'module-avatar__label',
 | |
|             `module-avatar__label--${size}`
 | |
|           )}
 | |
|           style={borderStyle}
 | |
|         >
 | |
|           {initials}
 | |
|         </div>
 | |
|       );
 | |
|     }
 | |
| 
 | |
|     return (
 | |
|       <div
 | |
|         className={classNames(
 | |
|           'module-avatar__icon',
 | |
|           `module-avatar__icon--${conversationType}`,
 | |
|           `module-avatar__icon--${size}`
 | |
|         )}
 | |
|         style={borderStyle}
 | |
|       />
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   public render() {
 | |
|     const {
 | |
|       avatarPath,
 | |
|       color,
 | |
|       size,
 | |
|       noteToSelf,
 | |
|       conversationType,
 | |
|     } = this.props;
 | |
|     const { imageBroken } = this.state;
 | |
| 
 | |
|     // If it's a direct conversation then we must have an identicon
 | |
|     const hasAvatar = avatarPath || conversationType === 'direct';
 | |
|     const hasImage = !noteToSelf && hasAvatar && !imageBroken;
 | |
| 
 | |
|     if (size !== 28 && size !== 36 && size !== 48 && size !== 80) {
 | |
|       throw new Error(`Size ${size} is not supported!`);
 | |
|     }
 | |
| 
 | |
|     return (
 | |
|       <div
 | |
|         className={classNames(
 | |
|           'module-avatar',
 | |
|           `module-avatar--${size}`,
 | |
|           hasImage ? 'module-avatar--with-image' : 'module-avatar--no-image',
 | |
|           !hasImage ? `module-avatar--${color}` : null
 | |
|         )}
 | |
|         onClick={e => {
 | |
|           this.onAvatarClickBound(e);
 | |
|         }}
 | |
|         role="button"
 | |
|       >
 | |
|         {hasImage ? this.renderAvatarOrIdenticon() : this.renderNoImage()}
 | |
|       </div>
 | |
|     );
 | |
|   }
 | |
| 
 | |
|   private onAvatarClick(e: any) {
 | |
|     if (this.props.onAvatarClick) {
 | |
|       e.stopPropagation();
 | |
|       this.props.onAvatarClick();
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   private renderAvatarOrIdenticon() {
 | |
|     const { avatarPath, conversationType } = this.props;
 | |
| 
 | |
|     // If it's a direct conversation then we must have an identicon
 | |
|     const hasAvatar = avatarPath || conversationType === 'direct';
 | |
| 
 | |
|     return hasAvatar && avatarPath
 | |
|       ? this.renderImage()
 | |
|       : this.renderIdenticon();
 | |
|   }
 | |
| 
 | |
|   private getBorderStyle(color?: string, width?: number) {
 | |
|     const borderWidth = typeof width === 'number' ? width : 3;
 | |
| 
 | |
|     return color
 | |
|       ? {
 | |
|           borderColor: color,
 | |
|           borderStyle: 'solid',
 | |
|           borderWidth: borderWidth,
 | |
|         }
 | |
|       : undefined;
 | |
|   }
 | |
| }
 |