Bluesky app fork with some witchin' additions 馃挮
at linkat-integration 243 lines 6.9 kB view raw
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})