import type { MouseEvent as ReactMouseEvent } from 'react'; import React, { memo, useRef } from '../../lib/teact/teact'; import { getActions } from '../../global'; import type { FC, TeactNode } from '../../lib/teact/teact'; import type { ApiChat, ApiPhoto, ApiUser } from '../../api/types'; import type { ObserveFn } from '../../hooks/useIntersectionObserver'; import type { StoryViewerOrigin } from '../../types'; import { ApiMediaFormat } from '../../api/types'; import { IS_TEST } from '../../config'; import { getChatAvatarHash, getChatTitle, getUserColorKey, getUserFullName, isUserId, isChatWithRepliesBot, isDeletedUser, getUserStoryHtmlId, } from '../../global/helpers'; import { getFirstLetters } from '../../util/textFormat'; import buildClassName, { createClassNameBuilder } from '../../util/buildClassName'; import renderText from './helpers/renderText'; import useMedia from '../../hooks/useMedia'; import useMediaTransition from '../../hooks/useMediaTransition'; import useLang from '../../hooks/useLang'; import useLastCallback from '../../hooks/useLastCallback'; import { useFastClick } from '../../hooks/useFastClick'; import OptimizedVideo from '../ui/OptimizedVideo'; import AvatarStoryCircle from './AvatarStoryCircle'; import './Avatar.scss'; const LOOP_COUNT = 3; export type AvatarSize = 'micro' | 'tiny' | 'mini' | 'small' | 'small-mobile' | 'medium' | 'large' | 'giant' | 'jumbo'; const cn = createClassNameBuilder('Avatar'); cn.media = cn('media'); cn.icon = cn('icon'); type OwnProps = { className?: string; size?: AvatarSize; peer?: ApiChat | ApiUser; photo?: ApiPhoto; text?: string; isSavedMessages?: boolean; withVideo?: boolean; withStory?: boolean; forPremiumPromo?: boolean; withStoryGap?: boolean; withStorySolid?: boolean; storyViewerOrigin?: StoryViewerOrigin; storyViewerMode?: 'full' | 'single-user' | 'disabled'; loopIndefinitely?: boolean; noPersonalPhoto?: boolean; observeIntersection?: ObserveFn; onClick?: (e: ReactMouseEvent, hasMedia: boolean) => void; }; const Avatar: FC = ({ className, size = 'large', peer, photo, text, isSavedMessages, withVideo, withStory, forPremiumPromo, withStoryGap, withStorySolid, storyViewerOrigin, storyViewerMode = 'single-user', loopIndefinitely, noPersonalPhoto, onClick, }) => { const { openStoryViewer } = getActions(); // eslint-disable-next-line no-null/no-null const ref = useRef(null); const videoLoopCountRef = useRef(0); const isPeerChat = peer && 'title' in peer; const user = peer && !isPeerChat ? peer as ApiUser : undefined; const chat = peer && isPeerChat ? peer as ApiChat : undefined; const isDeleted = user && isDeletedUser(user); const isReplies = peer && isChatWithRepliesBot(peer.id); const isForum = chat?.isForum; let imageHash: string | undefined; let videoHash: string | undefined; const shouldLoadVideo = withVideo && photo?.isVideo; const shouldFetchBig = size === 'jumbo'; if (!isSavedMessages && !isDeleted) { if ((user && !noPersonalPhoto) || chat) { imageHash = getChatAvatarHash(peer!, shouldFetchBig ? 'big' : undefined); } else if (photo) { imageHash = `photo${photo.id}?size=m`; if (photo.isVideo && withVideo) { videoHash = `videoAvatar${photo.id}?size=u`; } } } const imgBlobUrl = useMedia(imageHash, false, ApiMediaFormat.BlobUrl); const videoBlobUrl = useMedia(videoHash, !shouldLoadVideo, ApiMediaFormat.BlobUrl); const hasBlobUrl = Boolean(imgBlobUrl || videoBlobUrl); // `videoBlobUrl` can be taken from memory cache, so we need to check `shouldLoadVideo` again const shouldPlayVideo = Boolean(videoBlobUrl && shouldLoadVideo); const transitionClassNames = useMediaTransition(hasBlobUrl); const handleVideoEnded = useLastCallback((e) => { const video = e.currentTarget; if (!videoBlobUrl) return; if (loopIndefinitely) return; videoLoopCountRef.current += 1; if (videoLoopCountRef.current >= LOOP_COUNT) { video.style.display = 'none'; } }); const lang = useLang(); let content: TeactNode | undefined; const author = user ? getUserFullName(user) : (chat ? getChatTitle(lang, chat) : text); if (isSavedMessages) { content = ( ); } else if (isDeleted) { content = ( ); } else if (isReplies) { content = ( ); } else if (hasBlobUrl) { content = ( <> {author} {shouldPlayVideo && ( )} ); } else if (user) { const userFullName = getUserFullName(user); content = userFullName ? getFirstLetters(userFullName, 2) : undefined; } else if (chat) { const title = getChatTitle(lang, chat); content = title && getFirstLetters(title, isUserId(chat.id) ? 2 : 1); } else if (text) { content = getFirstLetters(text, 2); } const fullClassName = buildClassName( `Avatar size-${size}`, className, `color-bg-${getUserColorKey(peer)}`, isSavedMessages && 'saved-messages', isDeleted && 'deleted-account', isReplies && 'replies-bot-account', isForum && 'forum', ((withStory && user?.hasStories) || forPremiumPromo) && 'with-story-circle', withStorySolid && user?.hasStories && 'with-story-solid', withStorySolid && user?.hasUnreadStories && 'has-unread-story', onClick && 'interactive', (!isSavedMessages && !imgBlobUrl) && 'no-photo', ); const hasMedia = Boolean(isSavedMessages || imgBlobUrl); const { handleClick, handleMouseDown } = useFastClick((e: ReactMouseEvent) => { if (withStory && storyViewerMode !== 'disabled' && user?.hasStories) { e.stopPropagation(); openStoryViewer({ userId: user.id, isSingleUser: storyViewerMode === 'single-user', origin: storyViewerOrigin, }); return; } if (onClick) { onClick(e, hasMedia); } }); return (
{typeof content === 'string' ? renderText(content, [size === 'jumbo' ? 'hq_emoji' : 'emoji']) : content}
{withStory && user?.hasStories && ( )}
); }; export default memo(Avatar);