Something went wrong. Try again.
Bluesky app fork with some witchin' additions 💫 forked from jollywhoppers.com/witchsky.app
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363import {memo, useCallback, useEffect, useMemo} from 'react'import {Pressable, TouchableWithoutFeedback, View} from 'react-native'import Animated, { measure, type MeasuredDimensions, runOnJS, runOnUI, useAnimatedRef,} from 'react-native-reanimated'import {useSafeAreaInsets} from 'react-native-safe-area-context'import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api'import {utils} from '@bsky.app/alf'import {msg} from '@lingui/macro'import {useLingui} from '@lingui/react'import {useNavigation} from '@react-navigation/native'
import {useActorStatus} from '#/lib/actor-status'import {BACK_HITSLOP} from '#/lib/constants'import {useHaptics} from '#/lib/haptics'import {type NavigationProp} from '#/lib/routes/types'import {type Shadow} from '#/state/cache/types'import {useLightboxControls} from '#/state/lightbox'import {useEnableSquareAvatars} from '#/state/preferences/enable-square-avatars'import {useEnableSquareButtons} from '#/state/preferences/enable-square-buttons'import { maybeModifyHighQualityImage, useHighQualityImages,} from '#/state/preferences/high-quality-images'import {useSession} from '#/state/session'import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'import {UserAvatar} from '#/view/com/util/UserAvatar'import {UserBanner} from '#/view/com/util/UserBanner'import {atoms as a, platform, useTheme} from '#/alf'import {Button} from '#/components/Button'import {useDialogControl} from '#/components/Dialog'import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow'import {EditLiveDialog} from '#/components/live/EditLiveDialog'import {LiveIndicator} from '#/components/live/LiveIndicator'import {LiveStatusDialog} from '#/components/live/LiveStatusDialog'import {LabelsOnMe} from '#/components/moderation/LabelsOnMe'import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts'import {useAnalytics} from '#/analytics'import {IS_IOS} from '#/env'import {GrowableAvatar} from './GrowableAvatar'import {GrowableBanner} from './GrowableBanner'import {StatusBarShadow} from './StatusBarShadow'
interface Props { profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> moderation: ModerationDecision hideBackButton?: boolean isPlaceholderProfile?: boolean}
let ProfileHeaderShell = ({ children, profile, moderation, hideBackButton = false, isPlaceholderProfile,}: React.PropsWithChildren<Props>): React.ReactNode => { const t = useTheme() const ax = useAnalytics() const {currentAccount} = useSession() const {_} = useLingui() const {openLightbox} = useLightboxControls() const navigation = useNavigation<NavigationProp>() const {top: topInset} = useSafeAreaInsets() const playHaptic = useHaptics() const liveStatusControl = useDialogControl() const highQualityImages = useHighQualityImages() const enableSquareAvatars = useEnableSquareAvatars() const enableSquareButtons = useEnableSquareButtons()
const aviRef = useAnimatedRef() const bannerRef = useAnimatedRef<Animated.View>()
const onPressBack = useCallback(() => { if (navigation.canGoBack()) { navigation.goBack() } else { navigation.navigate('Home') } }, [navigation])
const _openLightbox = useCallback( ( uri: string, thumbRect: MeasuredDimensions | null, type: 'circle-avi' | 'image' = 'circle-avi', ) => { openLightbox({ images: [ { uri: maybeModifyHighQualityImage(uri, highQualityImages), thumbUri: maybeModifyHighQualityImage(uri, highQualityImages), thumbRect, dimensions: type === 'circle-avi' ? { // It's fine if it's actually smaller but we know it's 1:1. height: 1000, width: 1000, } : { // Banner aspect ratio is 3:1 width: 3000, height: 1000, }, thumbDimensions: null, type: enableSquareAvatars ? 'rect-avi' : 'circle-avi', }, ], index: 0, }) }, [openLightbox, highQualityImages, enableSquareAvatars], )
// theres probs a better way instead of just making a separate one but this works:tm: so its whatever const _openLightboxBanner = useCallback( (uri: string, thumbRect: MeasuredDimensions | null) => { openLightbox({ images: [ { uri: maybeModifyHighQualityImage(uri, highQualityImages), thumbUri: maybeModifyHighQualityImage(uri, highQualityImages), thumbRect, dimensions: thumbRect, thumbDimensions: null, type: 'image', }, ], index: 0, }) }, [openLightbox, highQualityImages], )
const isMe = useMemo( () => currentAccount?.did === profile.did, [currentAccount, profile], )
const live = useActorStatus(profile)
useEffect(() => { if (live.isActive) { ax.metric('live:view:profile', {subject: profile.did}) } }, [ax, live.isActive, profile.did])
const onPressAvi = useCallback(() => { if (live.isActive) { playHaptic('Light') ax.metric('live:card:open', {subject: profile.did, from: 'profile'}) liveStatusControl.open() } else { const modui = moderation.ui('avatar') const avatar = profile.avatar if (avatar && !(modui.blur && modui.noOverride)) { runOnUI(() => { 'worklet' const rect = measure(aviRef) runOnJS(_openLightbox)(avatar, rect) })() } } }, [ ax, profile, moderation, _openLightbox, aviRef, liveStatusControl, live, playHaptic, ])
const onPressBanner = useCallback(() => { const modui = moderation.ui('banner') const banner = profile.banner if (banner && !(modui.blur && modui.noOverride)) { runOnUI(() => { 'worklet' const rect = measure(bannerRef) runOnJS(_openLightboxBanner)(banner, rect) })() } }, [profile.banner, moderation, _openLightboxBanner, bannerRef])
return ( <View style={t.atoms.bg} pointerEvents={IS_IOS ? 'auto' : 'box-none'}> <View pointerEvents={IS_IOS ? 'auto' : 'box-none'} style={[a.relative, {height: 150}]}> <StatusBarShadow /> <GrowableBanner onPress={isPlaceholderProfile ? undefined : onPressBanner} bannerRef={bannerRef} backButton={ !hideBackButton && ( <Button testID="profileHeaderBackBtn" onPress={onPressBack} hitSlop={BACK_HITSLOP} label={_(msg`Back`)} style={[ a.absolute, a.pointer, { top: platform({ web: 10, default: topInset, }), left: platform({ web: 18, default: 12, }), }, ]}> {({hovered}) => ( <View style={[ a.align_center, a.justify_center, enableSquareButtons ? a.rounded_sm : a.rounded_full, { width: 31, height: 31, backgroundColor: utils.alpha('#000', 0.5), }, hovered && { backgroundColor: utils.alpha('#000', 0.75), }, ]}> <ArrowLeftIcon size="lg" fill="white" /> </View> )} </Button> ) }> {isPlaceholderProfile ? ( <LoadingPlaceholder width="100%" height="100%" style={{borderRadius: 0}} /> ) : ( <TouchableWithoutFeedback testID="profileHeaderBannerButton" onPress={onPressBanner} accessibilityRole="image" accessibilityLabel={_(msg`View ${profile.handle}'s banner`)} accessibilityHint=""> <View> <Animated.View ref={bannerRef} collapsable={false}> <UserBanner type={profile.associated?.labeler ? 'labeler' : 'default'} banner={profile.banner} moderation={moderation.ui('banner')} /> </Animated.View> </View> </TouchableWithoutFeedback> )} </GrowableBanner> </View>
{children}
{!isPlaceholderProfile && (isMe ? ( <LabelsOnMe type="account" labels={profile.labels} style={[ a.px_lg, a.pt_xs, a.pb_sm, IS_IOS ? a.pointer_events_auto : {pointerEvents: 'box-none'}, ]} /> ) : ( <ProfileHeaderAlerts moderation={moderation} style={[ a.px_lg, a.pt_xs, a.pb_sm, IS_IOS ? a.pointer_events_auto : {pointerEvents: 'box-none'}, ]} /> ))}
<GrowableAvatar style={[a.absolute, {top: 104, left: 10}]}> <Pressable testID="profileHeaderAviButton" onPress={onPressAvi} accessibilityRole="image" accessibilityLabel={_(msg`View ${profile.handle}'s avatar`)} accessibilityHint=""> <View style={[ t.atoms.bg, enableSquareAvatars ? a.rounded_md : a.rounded_full, { width: 94, height: 94, borderWidth: live.isActive ? 3 : 2, borderColor: live.isActive ? t.palette.negative_500 : t.atoms.bg.backgroundColor, }, profile.associated?.labeler && a.rounded_md, ]}> <Animated.View ref={aviRef} collapsable={false}> <UserAvatar type={profile.associated?.labeler ? 'labeler' : 'user'} size={live.isActive ? 88 : 90} avatar={profile.avatar} moderation={moderation.ui('avatar')} noBorder /> {live.isActive && <LiveIndicator size="large" />} </Animated.View> </View> </Pressable> </GrowableAvatar>
{live.isActive && (isMe ? ( <EditLiveDialog control={liveStatusControl} status={live} embed={live.embed} /> ) : ( <LiveStatusDialog control={liveStatusControl} status={live} embed={live.embed} profile={profile} onPressViewAvatar={() => { const modui = moderation.ui('avatar') const avatar = profile.avatar if (avatar && !(modui.blur && modui.noOverride)) { runOnUI(() => { 'worklet' const rect = measure(aviRef) runOnJS(_openLightbox)(avatar, rect) })() } }} /> ))} </View> )}
ProfileHeaderShell = memo(ProfileHeaderShell)export {ProfileHeaderShell}