import React from 'react'
import {View} from 'react-native'
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 :
}
type State = {
stage: 'hidden' | 'might-show' | 'showing' | 'might-hide' | 'hiding'
effect?: () => () => any
}
type Action =
| 'pressed'
| 'hovered'
| 'unhovered'
| 'show-timer-elapsed'
| 'hide-timer-elapsed'
| 'hide-animation-completed'
const SHOW_DELAY = 350
const SHOW_DURATION = 300
const HIDE_DELAY = 200
const HIDE_DURATION = 200
export function ProfileHoverCardInner(props: ProfileHoverCardProps) {
const {refs, floatingStyles} = useFloating({
middleware: floatingMiddlewares,
})
const [currentState, dispatch] = React.useReducer(
// Tip: console.log(state, action) when debugging.
(state: State, action: Action): State => {
// Regardless of which stage we're in, pressing always hides the card.
if (action === 'pressed') {
return {stage: 'hidden'}
}
if (state.stage === 'hidden') {
// Our story starts when the card is hidden.
// If the user hovers, we kick off a grace period before showing the card.
if (action === 'hovered') {
return {
stage: 'might-show',
effect() {
const id = setTimeout(
() => dispatch('show-timer-elapsed'),
SHOW_DELAY,
)
return () => {
clearTimeout(id)
}
},
}
}
}
if (state.stage === 'might-show') {
// We're in the grace period when we decide whether to show the card.
// At this point, two things can happen. Either the user unhovers, and
// we go back to hidden--or they linger enough that we'll show the card.
if (action === 'unhovered') {
return {stage: 'hidden'}
}
if (action === 'show-timer-elapsed') {
return {stage: 'showing'}
}
}
if (state.stage === 'showing') {
// We're showing the card now.
// If the user unhovers, we'll start a grace period before hiding the card.
if (action === 'unhovered') {
return {
stage: 'might-hide',
effect() {
const id = setTimeout(
() => dispatch('hide-timer-elapsed'),
HIDE_DELAY,
)
return () => clearTimeout(id)
},
}
}
}
if (state.stage === 'might-hide') {
// We're in the grace period when we decide whether to hide the card.
// At this point, two things can happen. Either the user hovers, and
// we go back to showing it--or they linger enough that we'll start hiding the card.
if (action === 'hovered') {
return {stage: 'showing'}
}
if (action === 'hide-timer-elapsed') {
return {
stage: 'hiding',
effect() {
const id = setTimeout(
() => dispatch('hide-animation-completed'),
HIDE_DURATION,
)
return () => clearTimeout(id)
},
}
}
}
if (state.stage === 'hiding') {
// We're currently playing the hiding animation.
// We'll ignore all inputs now and wait for the animation to finish.
// At that point, we'll hide the entire thing, going back to square one.
if (action === 'hide-animation-completed') {
return {stage: 'hidden'}
}
}
// Something else happened. Keep calm and carry on.
return state
},
{stage: 'hidden'},
)
React.useEffect(() => {
if (currentState.effect) {
const effect = currentState.effect
delete currentState.effect // Mark as completed
return effect()
}
}, [currentState])
const prefetchProfileQuery = usePrefetchProfileQuery()
const prefetchedProfile = React.useRef(false)
const prefetchIfNeeded = React.useCallback(async () => {
if (!prefetchedProfile.current) {
prefetchedProfile.current = true
prefetchProfileQuery(props.did)
}
}, [prefetchProfileQuery, props.did])
const onPointerEnterTarget = React.useCallback(() => {
prefetchIfNeeded()
dispatch('hovered')
}, [prefetchIfNeeded])
const onPointerLeaveTarget = React.useCallback(() => {
dispatch('unhovered')
}, [])
const onPointerEnterCard = React.useCallback(() => {
dispatch('hovered')
}, [])
const onPointerLeaveCard = React.useCallback(() => {
dispatch('unhovered')
}, [])
const onPress = React.useCallback(() => {
dispatch('pressed')
}, [])
const isVisible =
currentState.stage === 'showing' ||
currentState.stage === 'might-hide' ||
currentState.stage === 'hiding'
const animationStyle = {
animation:
currentState.stage === 'hiding'
? `avatarHoverFadeOut ${HIDE_DURATION}ms both`
: `avatarHoverFadeIn ${SHOW_DURATION}ms both`,
}
return (