Bluesky app fork with some witchin' additions 馃挮
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 3a9526d55eccacaf65fcbc885744d8ef4e50cf6a 312 lines 9.5 kB view raw
1import {memo, useCallback, useEffect, useMemo} from 'react' 2import {Pressable, View} from 'react-native' 3import Animated, { 4 measure, 5 type MeasuredDimensions, 6 runOnJS, 7 runOnUI, 8 useAnimatedRef, 9} from 'react-native-reanimated' 10import {useSafeAreaInsets} from 'react-native-safe-area-context' 11import {type AppBskyActorDefs, type ModerationDecision} from '@atproto/api' 12import {utils} from '@bsky.app/alf' 13import {msg} from '@lingui/macro' 14import {useLingui} from '@lingui/react' 15import {useNavigation} from '@react-navigation/native' 16 17import {useActorStatus} from '#/lib/actor-status' 18import {BACK_HITSLOP} from '#/lib/constants' 19import {useHaptics} from '#/lib/haptics' 20import {type NavigationProp} from '#/lib/routes/types' 21import {type Shadow} from '#/state/cache/types' 22import {useLightboxControls} from '#/state/lightbox' 23import {useSession} from '#/state/session' 24import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 25import {UserAvatar} from '#/view/com/util/UserAvatar' 26import {UserBanner} from '#/view/com/util/UserBanner' 27import {atoms as a, platform, useTheme} from '#/alf' 28import {Button} from '#/components/Button' 29import {useDialogControl} from '#/components/Dialog' 30import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow' 31import {EditLiveDialog} from '#/components/live/EditLiveDialog' 32import {LiveIndicator} from '#/components/live/LiveIndicator' 33import {LiveStatusDialog} from '#/components/live/LiveStatusDialog' 34import {LabelsOnMe} from '#/components/moderation/LabelsOnMe' 35import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts' 36import {useAnalytics} from '#/analytics' 37import {IS_IOS} from '#/env' 38import {GrowableAvatar} from './GrowableAvatar' 39import {GrowableBanner} from './GrowableBanner' 40import {StatusBarShadow} from './StatusBarShadow' 41 42interface Props { 43 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> 44 moderation: ModerationDecision 45 hideBackButton?: boolean 46 isPlaceholderProfile?: boolean 47} 48 49let ProfileHeaderShell = ({ 50 children, 51 profile, 52 moderation, 53 hideBackButton = false, 54 isPlaceholderProfile, 55}: React.PropsWithChildren<Props>): React.ReactNode => { 56 const t = useTheme() 57 const ax = useAnalytics() 58 const {currentAccount} = useSession() 59 const {_} = useLingui() 60 const {openLightbox} = useLightboxControls() 61 const navigation = useNavigation<NavigationProp>() 62 const {top: topInset} = useSafeAreaInsets() 63 const playHaptic = useHaptics() 64 const liveStatusControl = useDialogControl() 65 66 const aviRef = useAnimatedRef() 67 const bannerRef = useAnimatedRef<Animated.View>() 68 69 const onPressBack = useCallback(() => { 70 if (navigation.canGoBack()) { 71 navigation.goBack() 72 } else { 73 navigation.navigate('Home') 74 } 75 }, [navigation]) 76 77 const _openLightbox = useCallback( 78 ( 79 uri: string, 80 thumbRect: MeasuredDimensions | null, 81 type: 'circle-avi' | 'rect-avi' | 'image' = 'circle-avi', 82 ) => { 83 openLightbox({ 84 images: [ 85 { 86 uri, 87 thumbUri: uri, 88 thumbRect, 89 dimensions: 90 type === 'circle-avi' || type === 'rect-avi' 91 ? { 92 // It's fine if it's actually smaller but we know it's 1:1. 93 height: 1000, 94 width: 1000, 95 } 96 : { 97 // Banner aspect ratio is 3:1 98 width: 3000, 99 height: 1000, 100 }, 101 thumbDimensions: null, 102 type, 103 }, 104 ], 105 index: 0, 106 }) 107 }, 108 [openLightbox], 109 ) 110 111 const isMe = useMemo( 112 () => currentAccount?.did === profile.did, 113 [currentAccount, profile], 114 ) 115 116 const live = useActorStatus(profile) 117 118 useEffect(() => { 119 if (live.isActive) { 120 ax.metric('live:view:profile', {subject: profile.did}) 121 } 122 }, [ax, live.isActive, profile.did]) 123 124 const onPressAvi = useCallback(() => { 125 if (live.isActive) { 126 playHaptic('Light') 127 ax.metric('live:card:open', {subject: profile.did, from: 'profile'}) 128 liveStatusControl.open() 129 } else { 130 const modui = moderation.ui('avatar') 131 const avatar = profile.avatar 132 const type = profile.associated?.labeler ? 'rect-avi' : 'circle-avi' 133 if (avatar && !(modui.blur && modui.noOverride)) { 134 runOnUI(() => { 135 'worklet' 136 const rect = measure(aviRef) 137 runOnJS(_openLightbox)(avatar, rect, type) 138 })() 139 } 140 } 141 }, [ 142 ax, 143 profile, 144 moderation, 145 _openLightbox, 146 aviRef, 147 liveStatusControl, 148 live, 149 playHaptic, 150 ]) 151 152 const onPressBanner = useCallback(() => { 153 const modui = moderation.ui('banner') 154 const banner = profile.banner 155 if (banner && !(modui.blur && modui.noOverride)) { 156 runOnUI(() => { 157 'worklet' 158 const rect = measure(bannerRef) 159 runOnJS(_openLightbox)(banner, rect, 'image') 160 })() 161 } 162 }, [profile.banner, moderation, _openLightbox, bannerRef]) 163 164 return ( 165 <View style={t.atoms.bg} pointerEvents={IS_IOS ? 'auto' : 'box-none'}> 166 <View 167 pointerEvents={IS_IOS ? 'auto' : 'box-none'} 168 style={[a.relative, {height: 150}]}> 169 <StatusBarShadow /> 170 <GrowableBanner 171 onPress={isPlaceholderProfile ? undefined : onPressBanner} 172 bannerRef={bannerRef} 173 backButton={ 174 !hideBackButton && ( 175 <Button 176 testID="profileHeaderBackBtn" 177 onPress={onPressBack} 178 hitSlop={BACK_HITSLOP} 179 label={_(msg`Back`)} 180 style={[ 181 a.absolute, 182 a.pointer, 183 { 184 top: platform({ 185 web: 10, 186 default: topInset, 187 }), 188 left: platform({ 189 web: 18, 190 default: 12, 191 }), 192 }, 193 ]}> 194 {({hovered}) => ( 195 <View 196 style={[ 197 a.align_center, 198 a.justify_center, 199 a.rounded_full, 200 { 201 width: 31, 202 height: 31, 203 backgroundColor: utils.alpha('#000', 0.5), 204 }, 205 hovered && { 206 backgroundColor: utils.alpha('#000', 0.75), 207 }, 208 ]}> 209 <ArrowLeftIcon size="lg" fill="white" /> 210 </View> 211 )} 212 </Button> 213 ) 214 }> 215 {isPlaceholderProfile ? ( 216 <LoadingPlaceholder 217 width="100%" 218 height="100%" 219 style={{borderRadius: 0}} 220 /> 221 ) : ( 222 <UserBanner 223 type={profile.associated?.labeler ? 'labeler' : 'default'} 224 banner={profile.banner} 225 moderation={moderation.ui('banner')} 226 /> 227 )} 228 </GrowableBanner> 229 </View> 230 231 {children} 232 233 {!isPlaceholderProfile && 234 (isMe ? ( 235 <LabelsOnMe 236 type="account" 237 labels={profile.labels} 238 style={[ 239 a.px_lg, 240 a.pt_xs, 241 a.pb_sm, 242 IS_IOS ? a.pointer_events_auto : {pointerEvents: 'box-none'}, 243 ]} 244 /> 245 ) : ( 246 <ProfileHeaderAlerts 247 moderation={moderation} 248 style={[ 249 a.px_lg, 250 a.pt_xs, 251 a.pb_sm, 252 IS_IOS ? a.pointer_events_auto : {pointerEvents: 'box-none'}, 253 ]} 254 /> 255 ))} 256 257 <GrowableAvatar style={[a.absolute, {top: 104, left: 10}]}> 258 <Pressable 259 testID="profileHeaderAviButton" 260 onPress={onPressAvi} 261 accessibilityRole="image" 262 accessibilityLabel={_(msg`View ${profile.handle}'s avatar`)} 263 accessibilityHint=""> 264 <View 265 style={[ 266 t.atoms.bg, 267 a.rounded_full, 268 { 269 width: 94, 270 height: 94, 271 borderWidth: live.isActive ? 3 : 2, 272 borderColor: live.isActive 273 ? t.palette.negative_500 274 : t.atoms.bg.backgroundColor, 275 }, 276 profile.associated?.labeler && a.rounded_md, 277 ]}> 278 <Animated.View ref={aviRef} collapsable={false}> 279 <UserAvatar 280 type={profile.associated?.labeler ? 'labeler' : 'user'} 281 size={live.isActive ? 88 : 90} 282 avatar={profile.avatar} 283 moderation={moderation.ui('avatar')} 284 noBorder 285 /> 286 {live.isActive && <LiveIndicator size="large" />} 287 </Animated.View> 288 </View> 289 </Pressable> 290 </GrowableAvatar> 291 292 {live.isActive && 293 (isMe ? ( 294 <EditLiveDialog 295 control={liveStatusControl} 296 status={live} 297 embed={live.embed} 298 /> 299 ) : ( 300 <LiveStatusDialog 301 control={liveStatusControl} 302 status={live} 303 embed={live.embed} 304 profile={profile} 305 /> 306 ))} 307 </View> 308 ) 309} 310 311ProfileHeaderShell = memo(ProfileHeaderShell) 312export {ProfileHeaderShell}