forked from
jollywhoppers.com/witchsky.app
Bluesky app fork with some witchin' additions 馃挮
1import {memo, useMemo, useState} from 'react'
2import {type LayoutChangeEvent, StyleSheet, View} from 'react-native'
3import Animated, {
4 runOnJS,
5 useAnimatedReaction,
6 useAnimatedStyle,
7 withTiming,
8} from 'react-native-reanimated'
9import {useSafeAreaInsets} from 'react-native-safe-area-context'
10import {
11 type AppBskyActorDefs,
12 type AppBskyLabelerDefs,
13 moderateProfile,
14 type ModerationOpts,
15 type RichText as RichTextAPI,
16} from '@atproto/api'
17import {useIsFocused} from '@react-navigation/native'
18
19import {sanitizeHandle} from '#/lib/strings/handles'
20import {useProfileShadow} from '#/state/cache/profile-shadow'
21import {useModerationOpts} from '#/state/preferences/moderation-opts'
22import {useSetLightStatusBar} from '#/state/shell/light-status-bar'
23import {usePagerHeaderContext} from '#/view/com/pager/PagerHeaderContext'
24import {LoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
25import {atoms as a, useTheme} from '#/alf'
26import {Header} from '#/components/Layout'
27import * as ProfileCard from '#/components/ProfileCard'
28import {IS_NATIVE} from '#/env'
29import {
30 HeaderLabelerButtons,
31 ProfileHeaderLabeler,
32} from './ProfileHeaderLabeler'
33import {
34 HeaderStandardButtons,
35 ProfileHeaderStandard,
36} from './ProfileHeaderStandard'
37
38let ProfileHeaderLoading = (_props: {}): React.ReactNode => {
39 const t = useTheme()
40 return (
41 <View style={t.atoms.bg}>
42 <LoadingPlaceholder width="100%" height={150} style={{borderRadius: 0}} />
43 <View
44 style={[
45 t.atoms.bg,
46 {borderColor: t.atoms.bg.backgroundColor},
47 styles.avi,
48 ]}>
49 <LoadingPlaceholder width={90} height={90} style={styles.br45} />
50 </View>
51 <View style={styles.content}>
52 <View style={[styles.buttonsLine]}>
53 <LoadingPlaceholder width={140} height={34} style={styles.br50} />
54 </View>
55 </View>
56 </View>
57 )
58}
59ProfileHeaderLoading = memo(ProfileHeaderLoading)
60export {ProfileHeaderLoading}
61
62interface Props {
63 profile: AppBskyActorDefs.ProfileViewDetailed
64 labeler: AppBskyLabelerDefs.LabelerViewDetailed | undefined
65 descriptionRT: RichTextAPI | null
66 moderationOpts: ModerationOpts
67 hideBackButton?: boolean
68 isPlaceholderProfile?: boolean
69 setMinimumHeight: (height: number) => void
70}
71
72let ProfileHeader = ({setMinimumHeight, ...props}: Props): React.ReactNode => {
73 let content
74 if (props.profile.associated?.labeler) {
75 if (!props.labeler) {
76 content = <ProfileHeaderLoading />
77 } else {
78 content = <ProfileHeaderLabeler {...props} labeler={props.labeler} />
79 }
80 } else {
81 content = <ProfileHeaderStandard {...props} />
82 }
83
84 return (
85 <>
86 {IS_NATIVE && (
87 <MinimalHeader
88 onLayout={evt => setMinimumHeight(evt.nativeEvent.layout.height)}
89 profile={props.profile}
90 labeler={props.labeler}
91 hideBackButton={props.hideBackButton}
92 />
93 )}
94 {content}
95 </>
96 )
97}
98ProfileHeader = memo(ProfileHeader)
99export {ProfileHeader}
100
101const MinimalHeader = memo(function MinimalHeader({
102 onLayout,
103 profile: profileUnshadowed,
104 labeler,
105 hideBackButton = false,
106}: {
107 onLayout: (e: LayoutChangeEvent) => void
108 profile: AppBskyActorDefs.ProfileViewDetailed
109 labeler?: AppBskyLabelerDefs.LabelerViewDetailed
110 hideBackButton?: boolean
111}) {
112 const t = useTheme()
113 const insets = useSafeAreaInsets()
114 const ctx = usePagerHeaderContext()
115 const profile = useProfileShadow(profileUnshadowed)
116 const moderationOpts = useModerationOpts()
117 const moderation = useMemo(
118 () => (moderationOpts ? moderateProfile(profile, moderationOpts) : null),
119 [moderationOpts, profile],
120 )
121 const [visible, setVisible] = useState(false)
122 const [minimalHeaderHeight, setMinimalHeaderHeight] = useState(insets.top)
123 const isScreenFocused = useIsFocused()
124 if (!ctx) throw new Error('MinimalHeader cannot be used on web')
125 const {scrollY, headerHeight} = ctx
126
127 const animatedStyle = useAnimatedStyle(() => {
128 // if we don't yet have the min header height in JS, hide
129 if (!_WORKLET || minimalHeaderHeight === 0) {
130 return {
131 opacity: 0,
132 }
133 }
134 const pastThreshold = scrollY.get() > 100
135 return {
136 opacity: pastThreshold
137 ? withTiming(1, {duration: 75})
138 : withTiming(0, {duration: 75}),
139 transform: [
140 {
141 translateY: Math.min(
142 scrollY.get(),
143 headerHeight - minimalHeaderHeight,
144 ),
145 },
146 ],
147 }
148 })
149
150 useAnimatedReaction(
151 () => scrollY.get() > 100,
152 (value, prev) => {
153 if (prev !== value) {
154 runOnJS(setVisible)(value)
155 }
156 },
157 )
158
159 useSetLightStatusBar(isScreenFocused && !visible)
160
161 return (
162 <Animated.View
163 pointerEvents={visible ? 'auto' : 'none'}
164 aria-hidden={!visible}
165 accessibilityElementsHidden={!visible}
166 importantForAccessibility={visible ? 'auto' : 'no-hide-descendants'}
167 onLayout={evt => {
168 setMinimalHeaderHeight(evt.nativeEvent.layout.height)
169 onLayout(evt)
170 }}
171 style={[
172 a.absolute,
173 a.z_50,
174 t.atoms.bg,
175 {
176 top: 0,
177 left: 0,
178 right: 0,
179 paddingTop: insets.top,
180 },
181 animatedStyle,
182 ]}>
183 <Header.Outer noBottomBorder>
184 {hideBackButton ? <Header.MenuButton /> : <Header.BackButton />}
185 <Header.Content align="left">
186 {moderationOpts ? (
187 <ProfileCard.Name
188 profile={profile}
189 moderationOpts={moderationOpts}
190 textStyle={[a.font_bold]}
191 />
192 ) : (
193 <ProfileCard.NamePlaceholder />
194 )}
195 <Header.SubtitleText>
196 {sanitizeHandle(profile.handle, '@')}
197 </Header.SubtitleText>
198 </Header.Content>
199 {!profile.associated?.labeler
200 ? moderationOpts &&
201 moderation && (
202 <View style={[a.flex_row, a.justify_end, a.gap_xs]}>
203 <HeaderStandardButtons
204 profile={profile}
205 moderation={moderation}
206 moderationOpts={moderationOpts}
207 minimal
208 />
209 </View>
210 )
211 : labeler && (
212 <View style={[a.flex_row, a.justify_end, a.gap_xs]}>
213 <HeaderLabelerButtons profile={profile} minimal />
214 </View>
215 )}
216 </Header.Outer>
217 </Animated.View>
218 )
219})
220MinimalHeader.displayName = 'MinimalHeader'
221
222const styles = StyleSheet.create({
223 avi: {
224 position: 'absolute',
225 top: 110,
226 left: 10,
227 width: 94,
228 height: 94,
229 borderRadius: 47,
230 borderWidth: 2,
231 },
232 content: {
233 paddingTop: 12,
234 paddingHorizontal: 16,
235 paddingBottom: 8,
236 },
237 buttonsLine: {
238 flexDirection: 'row',
239 marginLeft: 'auto',
240 },
241 br45: {borderRadius: 45},
242 br50: {borderRadius: 50},
243})