mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
1import React, {memo} from 'react' 2import {StyleSheet, TouchableWithoutFeedback, View} from 'react-native' 3import {MeasuredDimensions, runOnJS, runOnUI} from 'react-native-reanimated' 4import {useSafeAreaInsets} from 'react-native-safe-area-context' 5import {AppBskyActorDefs, ModerationDecision} from '@atproto/api' 6import {msg} from '@lingui/macro' 7import {useLingui} from '@lingui/react' 8import {useNavigation} from '@react-navigation/native' 9 10import {BACK_HITSLOP} from '#/lib/constants' 11import {measureHandle, useHandleRef} from '#/lib/hooks/useHandleRef' 12import {NavigationProp} from '#/lib/routes/types' 13import {isIOS} from '#/platform/detection' 14import {Shadow} from '#/state/cache/types' 15import {useLightboxControls} from '#/state/lightbox' 16import {useSession} from '#/state/session' 17import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' 18import {UserAvatar} from '#/view/com/util/UserAvatar' 19import {UserBanner} from '#/view/com/util/UserBanner' 20import {atoms as a, platform, useTheme} from '#/alf' 21import {ArrowLeft_Stroke2_Corner0_Rounded as ArrowLeftIcon} from '#/components/icons/Arrow' 22import {LabelsOnMe} from '#/components/moderation/LabelsOnMe' 23import {ProfileHeaderAlerts} from '#/components/moderation/ProfileHeaderAlerts' 24import {GrowableAvatar} from './GrowableAvatar' 25import {GrowableBanner} from './GrowableBanner' 26import {StatusBarShadow} from './StatusBarShadow' 27 28interface Props { 29 profile: Shadow<AppBskyActorDefs.ProfileViewDetailed> 30 moderation: ModerationDecision 31 hideBackButton?: boolean 32 isPlaceholderProfile?: boolean 33} 34 35let ProfileHeaderShell = ({ 36 children, 37 profile, 38 moderation, 39 hideBackButton = false, 40 isPlaceholderProfile, 41}: React.PropsWithChildren<Props>): React.ReactNode => { 42 const t = useTheme() 43 const {currentAccount} = useSession() 44 const {_} = useLingui() 45 const {openLightbox} = useLightboxControls() 46 const navigation = useNavigation<NavigationProp>() 47 const {top: topInset} = useSafeAreaInsets() 48 49 const aviRef = useHandleRef() 50 51 const onPressBack = React.useCallback(() => { 52 if (navigation.canGoBack()) { 53 navigation.goBack() 54 } else { 55 navigation.navigate('Home') 56 } 57 }, [navigation]) 58 59 const _openLightbox = React.useCallback( 60 (uri: string, thumbRect: MeasuredDimensions | null) => { 61 openLightbox({ 62 images: [ 63 { 64 uri, 65 thumbUri: uri, 66 thumbRect, 67 dimensions: { 68 // It's fine if it's actually smaller but we know it's 1:1. 69 height: 1000, 70 width: 1000, 71 }, 72 thumbDimensions: null, 73 type: 'circle-avi', 74 }, 75 ], 76 index: 0, 77 }) 78 }, 79 [openLightbox], 80 ) 81 82 const onPressAvi = React.useCallback(() => { 83 const modui = moderation.ui('avatar') 84 const avatar = profile.avatar 85 if (avatar && !(modui.blur && modui.noOverride)) { 86 const aviHandle = aviRef.current 87 runOnUI(() => { 88 'worklet' 89 const rect = measureHandle(aviHandle) 90 runOnJS(_openLightbox)(avatar, rect) 91 })() 92 } 93 }, [profile, moderation, _openLightbox, aviRef]) 94 95 const isMe = React.useMemo( 96 () => currentAccount?.did === profile.did, 97 [currentAccount, profile], 98 ) 99 100 return ( 101 <View style={t.atoms.bg} pointerEvents={isIOS ? 'auto' : 'box-none'}> 102 <View 103 pointerEvents={isIOS ? 'auto' : 'box-none'} 104 style={[a.relative, {height: 150}]}> 105 <StatusBarShadow /> 106 <GrowableBanner 107 backButton={ 108 <> 109 {!hideBackButton && ( 110 <TouchableWithoutFeedback 111 testID="profileHeaderBackBtn" 112 onPress={onPressBack} 113 hitSlop={BACK_HITSLOP} 114 accessibilityRole="button" 115 accessibilityLabel={_(msg`Back`)} 116 accessibilityHint=""> 117 <View 118 style={[ 119 styles.backBtnWrapper, 120 { 121 top: platform({ 122 web: 10, 123 default: topInset, 124 }), 125 }, 126 ]}> 127 <ArrowLeftIcon size="lg" fill="white" /> 128 </View> 129 </TouchableWithoutFeedback> 130 )} 131 </> 132 }> 133 {isPlaceholderProfile ? ( 134 <LoadingPlaceholder 135 width="100%" 136 height="100%" 137 style={{borderRadius: 0}} 138 /> 139 ) : ( 140 <UserBanner 141 type={profile.associated?.labeler ? 'labeler' : 'default'} 142 banner={profile.banner} 143 moderation={moderation.ui('banner')} 144 /> 145 )} 146 </GrowableBanner> 147 </View> 148 149 {children} 150 151 {!isPlaceholderProfile && ( 152 <View 153 style={[a.px_lg, a.py_xs]} 154 pointerEvents={isIOS ? 'auto' : 'box-none'}> 155 {isMe ? ( 156 <LabelsOnMe type="account" labels={profile.labels} /> 157 ) : ( 158 <ProfileHeaderAlerts moderation={moderation} /> 159 )} 160 </View> 161 )} 162 163 <GrowableAvatar style={styles.aviPosition}> 164 <TouchableWithoutFeedback 165 testID="profileHeaderAviButton" 166 onPress={onPressAvi} 167 accessibilityRole="image" 168 accessibilityLabel={_(msg`View ${profile.handle}'s avatar`)} 169 accessibilityHint=""> 170 <View 171 style={[ 172 t.atoms.bg, 173 {borderColor: t.atoms.bg.backgroundColor}, 174 styles.avi, 175 profile.associated?.labeler && styles.aviLabeler, 176 ]}> 177 <View ref={aviRef} collapsable={false}> 178 <UserAvatar 179 type={profile.associated?.labeler ? 'labeler' : 'user'} 180 size={90} 181 avatar={profile.avatar} 182 moderation={moderation.ui('avatar')} 183 /> 184 </View> 185 </View> 186 </TouchableWithoutFeedback> 187 </GrowableAvatar> 188 </View> 189 ) 190} 191ProfileHeaderShell = memo(ProfileHeaderShell) 192export {ProfileHeaderShell} 193 194const styles = StyleSheet.create({ 195 backBtnWrapper: { 196 position: 'absolute', 197 left: 10, 198 width: 30, 199 height: 30, 200 overflow: 'hidden', 201 borderRadius: 15, 202 cursor: 'pointer', 203 backgroundColor: 'rgba(0, 0, 0, 0.5)', 204 alignItems: 'center', 205 justifyContent: 'center', 206 }, 207 backBtn: { 208 width: 30, 209 height: 30, 210 borderRadius: 15, 211 alignItems: 'center', 212 justifyContent: 'center', 213 }, 214 aviPosition: { 215 position: 'absolute', 216 top: 110, 217 left: 10, 218 }, 219 avi: { 220 width: 94, 221 height: 94, 222 borderRadius: 47, 223 borderWidth: 2, 224 }, 225 aviLabeler: { 226 borderRadius: 10, 227 }, 228})