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