import { memo, useEffect, useMemo, useState } from '../../../lib/teact/teact'; import type { ApiChat, ApiEmojiStatusType, ApiPeerColorOption, ApiPeerPhotos, ApiPeerProfileColorSet, ApiSavedGifts, ApiSticker, ApiTopic, ApiUser, ApiUserFullInfo, ApiUserStatus, } from '../../../api/types/index'; import type { IconName } from '../../../types/icons/index'; import type { AnimationLevel, ThemeKey } from '../../../types/index'; import { MediaViewerOrigin } from '../../../types/index'; import { getUserStatus, isAnonymousForwardsChat, isChatChannel, isSystemBot, isUserOnline, } from '../../../global/helpers/index'; import { getActions, withGlobal } from '../../../global/index'; import { selectChat, selectCurrentMessageList, selectCustomEmoji, selectPeer, selectPeerHasProfileBackground, selectPeerPhotos, selectPeerProfileColor, selectPeerSavedGifts, selectTabState, selectTheme, selectThreadMessagesCount, selectTopic, selectUser, selectUserFullInfo, selectUserStatus, } from '../../../global/selectors/index'; import { selectSharedSettings } from '../../../global/selectors/sharedState'; import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment'; import buildClassName from '../../../util/buildClassName'; import buildStyle from '../../../util/buildStyle'; import { captureEvents, SwipeDirection } from '../../../util/captureEvents'; import { MEMO_EMPTY_ARRAY } from '../../../util/memo'; import { resolveTransitionName } from '../../../util/resolveTransitionName'; import { REM } from '../helpers/mediaDimensions.ts'; import renderText from '../helpers/renderText.tsx'; import { useVtn } from '../../../hooks/animations/useVtn'; import useIntervalForceUpdate from '../../../hooks/schedulers/useIntervalForceUpdate'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useOldLang from '../../../hooks/useOldLang'; import usePreviousDeprecated from '../../../hooks/usePreviousDeprecated'; import useCustomEmoji from '../hooks/useCustomEmoji'; import usePhotosPreload from '../hooks/usePhotosPreload'; import Transition from '../../ui/Transition.tsx'; import Avatar from '../Avatar.tsx'; import FullNameTitle from '../FullNameTitle.tsx'; import Icon from '../icons/Icon.tsx'; import TopicIcon from '../TopicIcon.tsx'; import ProfilePhoto from './ProfilePhoto'; import ProfilePinnedGifts from './ProfilePinnedGifts.tsx'; import RadialPatternBackground from './RadialPatternBackground.tsx'; import './ProfileInfo.scss'; import styles from './ProfileInfo.module.scss'; const MAX_LEVEL_ICON = 90; type OwnProps = { isExpanded?: boolean; peerId: string; isForSettings?: boolean; canPlayVideo: boolean; isForMonoforum?: boolean; onExpand?: NoneToVoidFunction; }; type StateProps = { user?: ApiUser; userFullInfo?: ApiUserFullInfo; userStatus?: ApiUserStatus; chat?: ApiChat; mediaIndex?: number; avatarOwnerId?: string; topic?: ApiTopic; messagesCount?: number; animationLevel: AnimationLevel; emojiStatus?: ApiEmojiStatusType; emojiStatusSticker?: ApiSticker; emojiStatusSlug?: string; profilePhotos?: ApiPeerPhotos; profileColorOption?: ApiPeerColorOption; theme: ThemeKey; isPlain?: boolean; savedGifts?: ApiSavedGifts; }; const EMOJI_STATUS_SIZE = 24; const EMOJI_TOPIC_SIZE = 120; const LOAD_MORE_THRESHOLD = 3; const MAX_PHOTO_DASH_COUNT = 30; const STATUS_UPDATE_INTERVAL = 1000 * 60; // 1 min const PATTERN_Y_SHIFT = 8 * REM; const ProfileInfo = ({ isExpanded, isForSettings, canPlayVideo, user, userFullInfo, userStatus, chat, mediaIndex, avatarOwnerId, topic, messagesCount, animationLevel, emojiStatus, emojiStatusSticker, emojiStatusSlug, profilePhotos, peerId, isForMonoforum, profileColorOption, theme, isPlain, savedGifts, onExpand, }: OwnProps & StateProps) => { const { openMediaViewer, openPremiumModal, openStickerSet, openPrivacySettingsNoticeModal, loadMoreProfilePhotos, openUniqueGiftBySlug, openProfileRatingModal, loadPeerSavedGifts, } = getActions(); const oldLang = useOldLang(); const lang = useLang(); useIntervalForceUpdate(user ? STATUS_UPDATE_INTERVAL : undefined); const { createVtnStyle } = useVtn(); const photos = profilePhotos?.photos || MEMO_EMPTY_ARRAY; const prevMediaIndex = usePreviousDeprecated(mediaIndex); const prevAvatarOwnerId = usePreviousDeprecated(avatarOwnerId); const [hasSlideAnimation, setHasSlideAnimation] = useState(true); const [currentPhotoIndex, setCurrentPhotoIndex] = useState(0); const isFirst = photos.length <= 1 || currentPhotoIndex === 0; const isLast = photos.length <= 1 || currentPhotoIndex === photos.length - 1; const collectibleEmojiStatus = emojiStatus?.type === 'collectible' ? emojiStatus : undefined; const peer = user || chat; const { customEmoji: backgroundEmoji } = useCustomEmoji( collectibleEmojiStatus?.patternDocumentId || peer?.profileColor?.backgroundEmojiId, ); const profileColorSet = useMemo(() => { if (collectibleEmojiStatus) { return { bgColors: [collectibleEmojiStatus.centerColor, collectibleEmojiStatus.edgeColor], storyColors: [collectibleEmojiStatus.textColor, collectibleEmojiStatus.textColor], }; } const colors = profileColorOption && (theme === 'dark' ? profileColorOption.darkColors : profileColorOption.colors); if (!colors) return undefined; // Why are they reversed on the server? return { bgColors: [...colors.bgColors].reverse(), storyColors: [...colors.storyColors].reverse(), }; }, [profileColorOption, theme, collectibleEmojiStatus]); const pinnedGifts = useMemo(() => { return savedGifts?.gifts.filter((gift) => { if (gift.gift.type === 'starGiftUnique') { return gift.isPinned && gift.gift.slug !== collectibleEmojiStatus?.slug; } return gift.isPinned; }); }, [savedGifts, collectibleEmojiStatus?.slug]); useEffect(() => { if (photos.length - currentPhotoIndex <= LOAD_MORE_THRESHOLD) { loadMoreProfilePhotos({ peerId }); } }, [currentPhotoIndex, peerId, photos.length]); useEffect(() => { loadPeerSavedGifts({ peerId }); }, [peerId]); // Set the current avatar photo to the last selected photo in Media Viewer after it is closed useEffect(() => { if (prevAvatarOwnerId && prevMediaIndex !== undefined && mediaIndex === undefined) { setHasSlideAnimation(false); setCurrentPhotoIndex(prevMediaIndex); } }, [mediaIndex, prevMediaIndex, prevAvatarOwnerId]); // Deleting the last profile photo may result in an error useEffect(() => { if (currentPhotoIndex > photos.length) { setHasSlideAnimation(false); setCurrentPhotoIndex(Math.max(0, photos.length - 1)); } }, [currentPhotoIndex, photos.length]); // Reset photo index when collapsing useEffect(() => { if (!isExpanded) { setCurrentPhotoIndex(0); } }, [isExpanded]); usePhotosPreload(photos, currentPhotoIndex); const handleProfilePhotoClick = useLastCallback(() => { openMediaViewer({ isAvatarView: true, chatId: peerId, mediaIndex: currentPhotoIndex, origin: isForSettings ? MediaViewerOrigin.SettingsAvatar : MediaViewerOrigin.ProfileAvatar, }); }); const handleStatusClick = useLastCallback(() => { if (emojiStatusSlug) { openUniqueGiftBySlug({ slug: emojiStatusSlug }); return; } if (!peerId) { openStickerSet({ stickerSetInfo: emojiStatusSticker!.stickerSetInfo, }); return; } openPremiumModal({ fromUserId: peerId }); }); const selectPreviousMedia = useLastCallback(() => { if (isFirst) { return; } setHasSlideAnimation(true); setCurrentPhotoIndex(currentPhotoIndex - 1); }); const selectNextMedia = useLastCallback(() => { if (isLast) { return; } setHasSlideAnimation(true); setCurrentPhotoIndex(currentPhotoIndex + 1); }); const handleOpenGetReadDateModal = useLastCallback(() => { openPrivacySettingsNoticeModal({ chatId: chat!.id, isReadDate: false }); }); const handleRatingClick = useLastCallback((level: number) => { if (user) { openProfileRatingModal({ userId: user.id, level }); } }); const handleMinimizedAvatarClick = useLastCallback(() => { if (isForSettings) { handleProfilePhotoClick(); return; } onExpand?.(); }); function handleSelectFallbackPhoto() { if (!isFirst) return; setHasSlideAnimation(true); setCurrentPhotoIndex(photos.length - 1); } // Swipe gestures useEffect(() => { const element = document.querySelector(`.${styles.photoWrapper}`); if (!element) { return undefined; } return captureEvents(element, { selectorToPreventScroll: '.Profile, .settings-content', onSwipe: IS_TOUCH_ENV ? (e, direction) => { if (direction === SwipeDirection.Right) { selectPreviousMedia(); return true; } else if (direction === SwipeDirection.Left) { selectNextMedia(); return true; } return false; } : undefined, }); }, [selectNextMedia, selectPreviousMedia]); if (!user && !chat) { return undefined; } function renderTopic() { return (

{renderText(topic!.title)}

{messagesCount ? oldLang('Chat.Title.Topic', messagesCount, 'i') : oldLang('lng_forum_no_messages')}

); } function renderPhotoTabs() { const totalPhotosLength = Math.max(photos.length, profilePhotos?.count || 0); if (!photos || totalPhotosLength <= 1) { return undefined; } const enumerator = Array.from({ length: Math.min(totalPhotosLength, MAX_PHOTO_DASH_COUNT) }); const activeDashIndex = currentPhotoIndex >= MAX_PHOTO_DASH_COUNT ? MAX_PHOTO_DASH_COUNT - 1 : currentPhotoIndex; return (
{enumerator.map((_, i) => ( ))}
); } function renderPhoto(isActive?: boolean) { const photo = photos.length > 0 ? photos[currentPhotoIndex] : undefined; return ( ); } function renderUserRating() { if (!userFullInfo?.starsRating) return undefined; const level = userFullInfo.starsRating.level; const isNegative = level < 0; const onRatingClick = () => handleRatingClick(level); if (isNegative) { return ( ! ); } const safeLevel = Math.max(level, 1); const iconLevel = Math.min(safeLevel, MAX_LEVEL_ICON); const iconName = (iconLevel < 10 ? `rating-icons-level${iconLevel}` : `rating-icons-level${Math.floor(iconLevel / 10) * 10}`) as IconName; return ( {level} ); } function renderStatus() { const isAnonymousForwards = isAnonymousForwardsChat(peerId); const isSystemBotChat = isSystemBot(peerId); if (isAnonymousForwards || isSystemBotChat) return undefined; if (isForMonoforum) { return ( {lang('MonoforumStatus')} ); } if (user) { return (
{renderUserRating()} {getUserStatus(oldLang, user, userStatus)} {userStatus?.isReadDateRestrictedByMe && ( {oldLang('StatusHiddenShow')} )}
); } return ( { isChatChannel(chat!) ? oldLang('Subscribers', chat!.membersCount ?? 0, 'i') : oldLang('Members', chat!.membersCount ?? 0, 'i') } ); } if (topic) { return renderTopic(); } return (
{profileColorSet?.bgColors && ( )} {pinnedGifts && ( )} {isExpanded && (
{renderPhotoTabs()} {!isForSettings && profilePhotos?.personalPhoto && (
{oldLang(profilePhotos.personalPhoto.isVideo ? 'UserInfo.CustomVideo' : 'UserInfo.CustomPhoto')}
)} {isForSettings && profilePhotos?.fallbackPhoto && (
{!isLast && ( )} {oldLang(profilePhotos.fallbackPhoto.isVideo ? 'UserInfo.PublicVideo' : 'UserInfo.PublicPhoto')}
)} {renderPhoto} {!isFirst && (
)} {!isExpanded && ( )}
{(user || chat) && ( )} {renderStatus()}
); }; export default memo(withGlobal( (global, { peerId }): Complete => { const peer = selectPeer(global, peerId); const user = selectUser(global, peerId); const userFullInfo = user ? selectUserFullInfo(global, peerId) : undefined; const userStatus = selectUserStatus(global, peerId); const chat = selectChat(global, peerId); const profilePhotos = selectPeerPhotos(global, peerId); const { mediaIndex, chatId: avatarOwnerId } = selectTabState(global).mediaViewer; const isForum = chat?.isForum; const { threadId: currentTopicId } = selectCurrentMessageList(global) || {}; const topic = isForum && currentTopicId ? selectTopic(global, peerId, currentTopicId) : undefined; const { animationLevel } = selectSharedSettings(global); const emojiStatus = peer?.emojiStatus; const emojiStatusSticker = emojiStatus ? selectCustomEmoji(global, emojiStatus.documentId) : undefined; const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined; const profileColor = peer && selectPeerProfileColor(global, peer); const theme = selectTheme(global); const hasBackground = selectPeerHasProfileBackground(global, peerId); const savedGifts = selectPeerSavedGifts(global, peerId); return { user, userFullInfo, userStatus, chat, mediaIndex, avatarOwnerId, animationLevel, emojiStatusSticker, emojiStatusSlug, emojiStatus, profilePhotos, topic, messagesCount: topic ? selectThreadMessagesCount(global, peerId, currentTopicId!) : undefined, profileColorOption: profileColor, theme, isPlain: !hasBackground, savedGifts, }; }, )(ProfileInfo));