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