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>
 | 
						|
  );
 | 
						|
};
 |