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.
		
		
		
		
		
			
		
			
				
	
	
		
			130 lines
		
	
	
		
			3.6 KiB
		
	
	
	
		
			TypeScript
		
	
			
		
		
	
	
			130 lines
		
	
	
		
			3.6 KiB
		
	
	
	
		
			TypeScript
		
	
| import { useEffect, useState } from 'react';
 | |
| import { getSodiumRenderer } from '../../../session/crypto';
 | |
| import { allowOnlyOneAtATime } from '../../../session/utils/Promise';
 | |
| import { toHex } from '../../../session/utils/String';
 | |
| import { COLORS } from '../../../themes/constants/colors';
 | |
| import { getInitials } from '../../../util/getInitials';
 | |
| import { MemberAvatarPlaceHolder } from '../../icon/MemberAvatarPlaceHolder';
 | |
| 
 | |
| type Props = {
 | |
|   diameter: number;
 | |
|   name: string;
 | |
|   pubkey: string;
 | |
|   dataTestId?: string;
 | |
| };
 | |
| 
 | |
| /** NOTE we use libsodium instead of crypto.subtle.digest because node:crypto.subtle.digest does not work the same way and we need to unit test this component */
 | |
| const sha512FromPubkeyOneAtAtime = async (pubkey: string) => {
 | |
|   return allowOnlyOneAtATime(`sha512FromPubkey-${pubkey}`, async () => {
 | |
|     const sodium = await getSodiumRenderer();
 | |
|     const buf = sodium.crypto_hash_sha512(pubkey);
 | |
|     return toHex(buf);
 | |
|   });
 | |
| };
 | |
| 
 | |
| // do not do this on every avatar, just cache the values so we can reuse them across the app
 | |
| // key is the pubkey, value is the hash
 | |
| const cachedHashes = new Map<string, number>();
 | |
| 
 | |
| const avatarPlaceholderColors: Array<string> = Object.values(COLORS.PRIMARY);
 | |
| 
 | |
| function useHashBasedOnPubkey(pubkey: string) {
 | |
|   const [hash, setHash] = useState<number | undefined>(undefined);
 | |
|   const [loading, setIsLoading] = useState<boolean>(true);
 | |
| 
 | |
|   useEffect(() => {
 | |
|     const cachedHash = cachedHashes.get(pubkey);
 | |
| 
 | |
|     if (cachedHash) {
 | |
|       setHash(cachedHash);
 | |
|       setIsLoading(false);
 | |
|       return undefined;
 | |
|     }
 | |
|     setIsLoading(true);
 | |
|     let isInProgress = true;
 | |
| 
 | |
|     if (!pubkey) {
 | |
|       if (isInProgress) {
 | |
|         setIsLoading(false);
 | |
| 
 | |
|         setHash(undefined);
 | |
|       }
 | |
|       return undefined;
 | |
|     }
 | |
| 
 | |
|     // eslint-disable-next-line more/no-then
 | |
|     void sha512FromPubkeyOneAtAtime(pubkey).then(sha => {
 | |
|       if (isInProgress) {
 | |
|         setIsLoading(false);
 | |
|         // Generate the seed simulate the .hashCode as Java
 | |
|         if (sha) {
 | |
|           const hashed = parseInt(sha.substring(0, 12), 16) || 0;
 | |
|           setHash(hashed);
 | |
|           cachedHashes.set(pubkey, hashed);
 | |
| 
 | |
|           return;
 | |
|         }
 | |
|         setHash(undefined);
 | |
|       }
 | |
|     });
 | |
|     return () => {
 | |
|       isInProgress = false;
 | |
|     };
 | |
|   }, [pubkey]);
 | |
| 
 | |
|   return { loading, hash };
 | |
| }
 | |
| 
 | |
| export const AvatarPlaceHolder = (props: Props) => {
 | |
|   const { pubkey, diameter, name, dataTestId } = props;
 | |
| 
 | |
|   const { hash, loading } = useHashBasedOnPubkey(pubkey);
 | |
| 
 | |
|   const diameterWithoutBorder = diameter - 2;
 | |
|   const viewBox = `0 0 ${diameter} ${diameter}`;
 | |
|   const r = diameter / 2;
 | |
|   const rWithoutBorder = diameterWithoutBorder / 2;
 | |
| 
 | |
|   if (loading || !hash) {
 | |
|     // return avatar placeholder circle
 | |
|     return <MemberAvatarPlaceHolder dataTestId={dataTestId} />;
 | |
|   }
 | |
| 
 | |
|   const initials = getInitials(name);
 | |
| 
 | |
|   const fontSize = Math.floor(initials.length > 1 ? diameter * 0.4 : diameter * 0.5);
 | |
| 
 | |
|   const bgColorIndex = hash % avatarPlaceholderColors.length;
 | |
| 
 | |
|   const bgColor = avatarPlaceholderColors[bgColorIndex];
 | |
| 
 | |
|   return (
 | |
|     <svg viewBox={viewBox} data-testid={dataTestId}>
 | |
|       <g id="UrTavla">
 | |
|         <circle
 | |
|           cx={r}
 | |
|           cy={r}
 | |
|           r={rWithoutBorder}
 | |
|           fill={bgColor}
 | |
|           shapeRendering="geometricPrecision"
 | |
|           stroke={'var(--avatar-border-color)'}
 | |
|           strokeWidth="1"
 | |
|         />
 | |
|         <text
 | |
|           fontSize={fontSize}
 | |
|           x="50%"
 | |
|           y="50%"
 | |
|           fill="var(--white-color)"
 | |
|           textAnchor="middle"
 | |
|           stroke="var(--white-color)"
 | |
|           strokeWidth={1}
 | |
|           alignmentBaseline="central"
 | |
|           height={fontSize}
 | |
|         >
 | |
|           {initials}
 | |
|         </text>
 | |
|       </g>
 | |
|     </svg>
 | |
|   );
 | |
| };
 |