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

Configure Feed

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

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