import React from 'react' import {View} from 'react-native' import Animated, {FadeIn, FadeOut} from 'react-native-reanimated' import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {flip, offset, shift, size, useFloating} from '@floating-ui/react-dom' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {makeProfileLink} from '#/lib/routes/links' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {pluralize} from '#/lib/strings/helpers' import {useModerationOpts} from '#/state/queries/preferences' import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile' import {useSession} from '#/state/session' import {useProfileShadow} from 'state/cache/profile-shadow' import {formatCount} from '#/view/com/util/numeric/format' import {UserAvatar} from '#/view/com/util/UserAvatar' import {ProfileHeaderHandle} from '#/screens/Profile/Header/Handle' import {atoms as a, useTheme} from '#/alf' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import {useFollowMethods} from '#/components/hooks/useFollowMethods' import {useRichText} from '#/components/hooks/useRichText' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {InlineLinkText, Link} from '#/components/Link' import {Loader} from '#/components/Loader' import {Portal} from '#/components/Portal' import {RichText} from '#/components/RichText' import {Text} from '#/components/Typography' import {ProfileHoverCardProps} from './types' const floatingMiddlewares = [ offset(4), flip({padding: 16}), shift({padding: 16}), size({ padding: 16, apply({availableWidth, availableHeight, elements}) { Object.assign(elements.floating.style, { maxWidth: `${availableWidth}px`, maxHeight: `${availableHeight}px`, }) }, }), ] const isTouchDevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0 export function ProfileHoverCard(props: ProfileHoverCardProps) { return isTouchDevice ? props.children : } export function ProfileHoverCardInner(props: ProfileHoverCardProps) { const [hovered, setHovered] = React.useState(false) const {refs, floatingStyles} = useFloating({ middleware: floatingMiddlewares, }) const prefetchProfileQuery = usePrefetchProfileQuery() const prefetchedProfile = React.useRef(false) const targetHovered = React.useRef(false) const cardHovered = React.useRef(false) const targetClicked = React.useRef(false) const showTimeout = React.useRef() const onPointerEnterTarget = React.useCallback(() => { showTimeout.current = setTimeout(async () => { targetHovered.current = true if (prefetchedProfile.current) { // if we're navigating if (targetClicked.current) return setHovered(true) } else { await prefetchProfileQuery(props.did) if (targetHovered.current) { setHovered(true) } prefetchedProfile.current = true } }, 350) }, [props.did, prefetchProfileQuery]) const onPointerEnterCard = React.useCallback(() => { cardHovered.current = true // if we're navigating if (targetClicked.current) return setHovered(true) }, []) const onPointerLeaveTarget = React.useCallback(() => { clearTimeout(showTimeout.current) targetHovered.current = false setTimeout(() => { if (cardHovered.current) return setHovered(false) }, 100) }, []) const onPointerLeaveCard = React.useCallback(() => { cardHovered.current = false setTimeout(() => { if (targetHovered.current) return setHovered(false) }, 100) }, []) const onClickTarget = React.useCallback(() => { targetClicked.current = true setHovered(false) }, []) const hide = React.useCallback(() => { setHovered(false) }, []) return (
{props.children} {hovered && (
)}
) } function Card({did, hide}: {did: string; hide: () => void}) { const t = useTheme() const profile = useProfileQuery({did}) const moderationOpts = useModerationOpts() const data = profile.data return ( {data && moderationOpts ? ( ) : ( )} ) } function Inner({ profile, moderationOpts, hide, }: { profile: AppBskyActorDefs.ProfileViewDetailed moderationOpts: ModerationOpts hide: () => void }) { const t = useTheme() const {_} = useLingui() const {currentAccount} = useSession() const moderation = React.useMemo( () => moderateProfile(profile, moderationOpts), [profile, moderationOpts], ) const [descriptionRT] = useRichText(profile.description ?? '') const profileShadow = useProfileShadow(profile) const {follow, unfollow} = useFollowMethods({ profile: profileShadow, logContext: 'ProfileHoverCard', }) const blockHide = profile.viewer?.blocking || profile.viewer?.blockedBy const following = formatCount(profile.followsCount || 0) const followers = formatCount(profile.followersCount || 0) const pluralizedFollowers = pluralize(profile.followersCount || 0, 'follower') const profileURL = makeProfileLink({ did: profile.did, handle: profile.handle, }) const isMe = React.useMemo( () => currentAccount?.did === profile.did, [currentAccount, profile], ) return ( {!isMe && ( )} {sanitizeDisplayName( profile.displayName || sanitizeHandle(profile.handle), moderation.ui('displayName'), )} {!blockHide && ( <> {followers} {pluralizedFollowers} {following} following {profile.description?.trim() && !moderation.ui('profileView').blur ? ( ) : undefined} )} ) }