mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
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})