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})