mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at tooltip 292 lines 8.4 kB view raw
1import {memo, useCallback, useEffect, useMemo} from 'react' 2import {StyleSheet, 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 {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 {GrowableAvatar} from './GrowableAvatar' 37import {GrowableBanner} from './GrowableBanner' 38import {StatusBarShadow} from './StatusBarShadow' 39 40interface Props { 41 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> 42 moderation: ModerationDecision 43 hideBackButton?: boolean 44 isPlaceholderProfile?: boolean 45} 46 47let ProfileHeaderShell = ({ 48 children, 49 profile, 50 moderation, 51 hideBackButton = false, 52 isPlaceholderProfile, 53}: React.PropsWithChildren<Props>): React.ReactNode => { 54 const t = useTheme() 55 const {currentAccount} = useSession() 56 const {_} = useLingui() 57 const {openLightbox} = useLightboxControls() 58 const navigation = useNavigation<NavigationProp>() 59 const {top: topInset} = useSafeAreaInsets() 60 const playHaptic = useHaptics() 61 const liveStatusControl = useDialogControl() 62 63 const aviRef = useAnimatedRef() 64 65 const onPressBack = useCallback(() => { 66 if (navigation.canGoBack()) { 67 navigation.goBack() 68 } else { 69 navigation.navigate('Home') 70 } 71 }, [navigation]) 72 73 const _openLightbox = useCallback( 74 (uri: string, thumbRect: MeasuredDimensions | null) => { 75 openLightbox({ 76 images: [ 77 { 78 uri, 79 thumbUri: uri, 80 thumbRect, 81 dimensions: { 82 // It's fine if it's actually smaller but we know it's 1:1. 83 height: 1000, 84 width: 1000, 85 }, 86 thumbDimensions: null, 87 type: 'circle-avi', 88 }, 89 ], 90 index: 0, 91 }) 92 }, 93 [openLightbox], 94 ) 95 96 const isMe = useMemo( 97 () => currentAccount?.did === profile.did, 98 [currentAccount, profile], 99 ) 100 101 const live = useActorStatus(profile) 102 103 useEffect(() => { 104 if (live.isActive) { 105 logger.metric( 106 'live:view:profile', 107 {subject: profile.did}, 108 {statsig: true}, 109 ) 110 } 111 }, [live.isActive, profile.did]) 112 113 const onPressAvi = useCallback(() => { 114 if (live.isActive) { 115 playHaptic('Light') 116 logger.metric( 117 'live:card:open', 118 {subject: profile.did, from: 'profile'}, 119 {statsig: true}, 120 ) 121 liveStatusControl.open() 122 } else { 123 const modui = moderation.ui('avatar') 124 const avatar = profile.avatar 125 if (avatar && !(modui.blur && modui.noOverride)) { 126 runOnUI(() => { 127 'worklet' 128 const rect = measure(aviRef) 129 runOnJS(_openLightbox)(avatar, rect) 130 })() 131 } 132 } 133 }, [ 134 profile, 135 moderation, 136 _openLightbox, 137 aviRef, 138 liveStatusControl, 139 live, 140 playHaptic, 141 ]) 142 143 return ( 144 <View style={t.atoms.bg} pointerEvents={isIOS ? 'auto' : 'box-none'}> 145 <View 146 pointerEvents={isIOS ? 'auto' : 'box-none'} 147 style={[a.relative, {height: 150}]}> 148 <StatusBarShadow /> 149 <GrowableBanner 150 backButton={ 151 <> 152 {!hideBackButton && ( 153 <TouchableWithoutFeedback 154 testID="profileHeaderBackBtn" 155 onPress={onPressBack} 156 hitSlop={BACK_HITSLOP} 157 accessibilityRole="button" 158 accessibilityLabel={_(msg`Back`)} 159 accessibilityHint=""> 160 <View 161 style={[ 162 styles.backBtnWrapper, 163 { 164 top: platform({ 165 web: 10, 166 default: topInset, 167 }), 168 }, 169 ]}> 170 <ArrowLeftIcon size="lg" fill="white" /> 171 </View> 172 </TouchableWithoutFeedback> 173 )} 174 </> 175 }> 176 {isPlaceholderProfile ? ( 177 <LoadingPlaceholder 178 width="100%" 179 height="100%" 180 style={{borderRadius: 0}} 181 /> 182 ) : ( 183 <UserBanner 184 type={profile.associated?.labeler ? 'labeler' : 'default'} 185 banner={profile.banner} 186 moderation={moderation.ui('banner')} 187 /> 188 )} 189 </GrowableBanner> 190 </View> 191 192 {children} 193 194 {!isPlaceholderProfile && ( 195 <View 196 style={[a.px_lg, a.py_xs]} 197 pointerEvents={isIOS ? 'auto' : 'box-none'}> 198 {isMe ? ( 199 <LabelsOnMe type="account" labels={profile.labels} /> 200 ) : ( 201 <ProfileHeaderAlerts moderation={moderation} /> 202 )} 203 </View> 204 )} 205 206 <GrowableAvatar style={styles.aviPosition}> 207 <TouchableWithoutFeedback 208 testID="profileHeaderAviButton" 209 onPress={onPressAvi} 210 accessibilityRole="image" 211 accessibilityLabel={_(msg`View ${profile.handle}'s avatar`)} 212 accessibilityHint=""> 213 <View 214 style={[ 215 t.atoms.bg, 216 a.rounded_full, 217 { 218 borderWidth: live.isActive ? 3 : 2, 219 borderColor: live.isActive 220 ? t.palette.negative_500 221 : t.atoms.bg.backgroundColor, 222 }, 223 styles.avi, 224 profile.associated?.labeler && styles.aviLabeler, 225 ]}> 226 <Animated.View ref={aviRef} collapsable={false}> 227 <UserAvatar 228 type={profile.associated?.labeler ? 'labeler' : 'user'} 229 size={live.isActive ? 88 : 90} 230 avatar={profile.avatar} 231 moderation={moderation.ui('avatar')} 232 /> 233 {live.isActive && <LiveIndicator size="large" />} 234 </Animated.View> 235 </View> 236 </TouchableWithoutFeedback> 237 </GrowableAvatar> 238 239 {live.isActive && 240 (isMe ? ( 241 <EditLiveDialog 242 control={liveStatusControl} 243 status={live} 244 embed={live.embed} 245 /> 246 ) : ( 247 <LiveStatusDialog 248 control={liveStatusControl} 249 status={live} 250 embed={live.embed} 251 profile={profile} 252 /> 253 ))} 254 </View> 255 ) 256} 257ProfileHeaderShell = memo(ProfileHeaderShell) 258export {ProfileHeaderShell} 259 260const styles = StyleSheet.create({ 261 backBtnWrapper: { 262 position: 'absolute', 263 left: 10, 264 width: 30, 265 height: 30, 266 overflow: 'hidden', 267 borderRadius: 15, 268 cursor: 'pointer', 269 backgroundColor: 'rgba(0, 0, 0, 0.5)', 270 alignItems: 'center', 271 justifyContent: 'center', 272 }, 273 backBtn: { 274 width: 30, 275 height: 30, 276 borderRadius: 15, 277 alignItems: 'center', 278 justifyContent: 'center', 279 }, 280 aviPosition: { 281 position: 'absolute', 282 top: 110, 283 left: 10, 284 }, 285 avi: { 286 width: 94, 287 height: 94, 288 }, 289 aviLabeler: { 290 borderRadius: 10, 291 }, 292})