Lil baby button checkpoint

This commit is contained in:
Eric Bailey
2024-12-10 20:23:56 -06:00
parent d4efa913f4
commit 4d380a0da1
3 changed files with 195 additions and 100 deletions
+3 -1
View File
@@ -21,6 +21,7 @@ export * as Header from '#/components/Layout/Header'
export type ScreenProps = React.ComponentProps<typeof View> & {
style?: StyleProp<ViewStyle>
noInsetTop?: boolean
}
/**
@@ -28,6 +29,7 @@ export type ScreenProps = React.ComponentProps<typeof View> & {
*/
export const Screen = React.memo(function Screen({
style,
noInsetTop,
...props
}: ScreenProps) {
const {top} = useSafeAreaInsets()
@@ -35,7 +37,7 @@ export const Screen = React.memo(function Screen({
<>
{isWeb && <WebCenterBorders />}
<View
style={[a.util_screen_outer, {paddingTop: top}, style]}
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
{...props}
/>
</>
+64 -96
View File
@@ -1,11 +1,12 @@
import React, {useCallback, useMemo} from 'react'
import {Pressable, StyleSheet, View} from 'react-native'
import {Pressable, StyleSheet, View, ScrollView} from 'react-native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {msg, Plural, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useIsFocused, useNavigation} from '@react-navigation/native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useQueryClient} from '@tanstack/react-query'
import {useAnimatedRef} from 'react-native-reanimated'
import {HITSLOP_20} from '#/lib/constants'
import {useHaptics} from '#/lib/haptics'
@@ -38,7 +39,6 @@ import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {truncateAndInvalidate} from '#/state/queries/util'
import {useSession} from '#/state/session'
import {useComposerControls} from '#/state/shell/composer'
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
import {PostFeed} from '#/view/com/posts/PostFeed'
import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
import {EmptyState} from '#/view/com/util/EmptyState'
@@ -66,7 +66,7 @@ import * as Menu from '#/components/Menu'
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
import {RichText} from '#/components/RichText'
const SECTION_TITLES = ['Posts']
import {ProfileSubpageHeader as NewHeader} from '#/screens/Profile/components/ProfileSubpageHeader'
interface SectionRef {
scrollToTop: () => void
@@ -125,8 +125,10 @@ export function ProfileFeedScreen(props: Props) {
}
return resolvedUri ? (
<Layout.Screen>
<Layout.Screen noInsetTop>
<Layout.Center>
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
</Layout.Center>
</Layout.Screen>
) : (
<Layout.Screen>
@@ -164,7 +166,6 @@ export function ProfileFeedScreenInner({
const reportDialogControl = useReportDialogControl()
const {openComposer} = useComposerControls()
const playHaptic = useHaptics()
const feedSectionRef = React.useRef<SectionRef>(null)
const isScreenFocused = useIsFocused()
const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
@@ -257,18 +258,9 @@ export function ProfileFeedScreenInner({
reportDialogControl.open()
}, [reportDialogControl])
const onCurrentPageSelected = React.useCallback(
(index: number) => {
if (index === 0) {
feedSectionRef.current?.scrollToTop()
}
},
[feedSectionRef],
)
const renderHeader = useCallback(() => {
return (
<>
<View>
<ProfileSubpageHeader
isLoading={false}
href={feedInfo.route.href}
@@ -370,12 +362,13 @@ export function ProfileFeedScreenInner({
</Menu.Root>
</View>
</ProfileSubpageHeader>
<AboutSection
feedOwnerDid={feedInfo.creatorDid}
feedRkey={feedInfo.route.params.rkey}
feedInfo={feedInfo}
/>
</>
</View>
)
}, [
_,
@@ -392,6 +385,34 @@ export function ProfileFeedScreenInner({
isPending,
])
const feed = `feedgen|${feedInfo.uri}` as FeedDescriptor
const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const queryClient = useQueryClient()
const feedFeedback = useFeedFeedback(feed, hasSession)
const scrollElRef = useAnimatedRef() as ListRef
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({
animated: isNative,
offset: 0, // -headerHeight,
})
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
setHasNew(false)
}, [scrollElRef, queryClient, feed, setHasNew])
React.useEffect(() => {
if (!isScreenFocused) {
return
}
return listenSoftReset(onScrollToTop)
}, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
}, [_])
return (
<>
<ReportDialog
@@ -402,21 +423,35 @@ export function ProfileFeedScreenInner({
cid: feedInfo.cid,
}}
/>
<PagerWithHeader
items={SECTION_TITLES}
isHeaderReady={true}
renderHeader={renderHeader}
onCurrentPageSelected={onCurrentPageSelected}>
{({headerHeight, scrollElRef, isFocused}) => (
<FeedSection
ref={feedSectionRef}
feed={`feedgen|${feedInfo.uri}`}
headerHeight={headerHeight}
scrollElRef={scrollElRef as ListRef}
isFocused={isScreenFocused && isFocused}
<NewHeader
title={feedInfo.displayName}
avatar={feedInfo.avatar}
creator={{did: feedInfo.creatorDid, handle: feedInfo.creatorHandle}}
likeCount={feedInfo.likeCount || 0}
/>
<FeedFeedbackProvider value={feedFeedback}>
<PostFeed
feed={feed}
pollInterval={60e3}
disablePoll={hasNew}
onHasNew={setHasNew}
scrollElRef={scrollElRef}
onScrolledDownChange={setIsScrolledDown}
renderEmptyState={renderPostsEmpty}
ListHeaderComponent={renderHeader}
/>
</FeedFeedbackProvider>
{(isScrolledDown || hasNew) && (
<LoadLatestBtn
onPress={onScrollToTop}
label={_(msg`Load new posts`)}
showIndicator={hasNew}
/>
)}
</PagerWithHeader>
{hasSession && (
<FAB
testID="composeFAB"
@@ -437,73 +472,6 @@ export function ProfileFeedScreenInner({
)
}
interface FeedSectionProps {
feed: FeedDescriptor
headerHeight: number
scrollElRef: ListRef
isFocused: boolean
}
const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
function FeedSectionImpl({feed, headerHeight, scrollElRef, isFocused}, ref) {
const {_} = useLingui()
const [hasNew, setHasNew] = React.useState(false)
const [isScrolledDown, setIsScrolledDown] = React.useState(false)
const queryClient = useQueryClient()
const isScreenFocused = useIsFocused()
const {hasSession} = useSession()
const feedFeedback = useFeedFeedback(feed, hasSession)
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({
animated: isNative,
offset: -headerHeight,
})
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
React.useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop,
}))
React.useEffect(() => {
if (!isScreenFocused) {
return
}
return listenSoftReset(onScrollToTop)
}, [onScrollToTop, isScreenFocused])
const renderPostsEmpty = useCallback(() => {
return <EmptyState icon="hashtag" message={_(msg`This feed is empty.`)} />
}, [_])
return (
<View>
<FeedFeedbackProvider value={feedFeedback}>
<PostFeed
enabled={isFocused}
feed={feed}
pollInterval={60e3}
disablePoll={hasNew}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScrolledDownChange={setIsScrolledDown}
renderEmptyState={renderPostsEmpty}
headerOffset={headerHeight}
/>
</FeedFeedbackProvider>
{(isScrolledDown || hasNew) && (
<LoadLatestBtn
onPress={onScrollToTop}
label={_(msg`Load new posts`)}
showIndicator={hasNew}
/>
)}
</View>
)
},
)
function AboutSection({
feedOwnerDid,
feedRkey,
@@ -0,0 +1,125 @@
import {View, Text as RNText} from 'react-native'
import {useLingui} from '@lingui/react'
import {msg, plural, Trans} from '@lingui/macro'
import {useSafeAreaInsets} from 'react-native-safe-area-context'
import * as Layout from '#/components/Layout'
import {atoms as a, useTheme, useBreakpoints, web} from '#/alf'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {Text} from '#/components/Typography'
import {Pin_Stroke2_Corner0_Rounded as Pin} from '#/components/icons/Pin'
import {ChevronBottom_Stroke2_Corner0_Rounded as ChevronDown} from '#/components/icons/Chevron'
import {Button, ButtonIcon} from '#/components/Button'
import {sanitizeHandle} from '#/lib/strings/handles'
export function ProfileSubpageHeader({
title,
avatar,
creator,
likeCount,
}: {
title: string
avatar?: string
creator: {did: string; handle: string}
likeCount: number
}) {
const t = useTheme()
const {gtPhone, gtMobile} = useBreakpoints()
const {_} = useLingui()
const {top} = useSafeAreaInsets()
return (
<Layout.Center
style={[t.atoms.bg, a.z_10, {paddingTop: top}, web([a.sticky, a.z_10, {top: 0}])]}
>
<Layout.Header.Outer>
<Layout.Header.BackButton />
<Layout.Header.Content align="left">
<Button
label={_(msg`Open feed info screen`)}
style={[
a.justify_start,
{
paddingVertical: 6,
paddingHorizontal: 8,
paddingRight: 12,
},
]}>
{({hovered}) => (
<>
<View
style={[
a.absolute,
a.inset_0,
a.rounded_sm,
a.transition_transform,
t.atoms.bg_contrast_25,
hovered && {
transform: [{scaleX: 1.01}, {scaleY: 1.1}],
},
]}
/>
<View style={[a.flex_1, a.flex_row, a.align_center, a.gap_sm]}>
{avatar && (
<UserAvatar size={32} type="algo" avatar={avatar} />
)}
<View style={[a.flex_1]}>
{/* Should roughly matchl Layout.Header.TitleText */}
<Text
style={[
a.text_md,
a.font_heavy,
a.leading_tight,
gtMobile && a.text_lg,
]}
numberOfLines={2}>
{title}
</Text>
{/* Should roughly matchl Layout.Header.SubtitleText */}
<Text
style={[
a.flex_1,
a.text_xs,
a.leading_snug,
t.atoms.text_contrast_medium,
gtPhone && a.text_sm,
]}>
<RNText numberOfLines={1}>
<Trans>By {sanitizeHandle(creator.handle, '@')}</Trans>
</RNText>
{' • '}
<RNText numberOfLines={1}>
{plural(likeCount, {
one: '# like',
other: '# likes',
})}
</RNText>
</Text>
</View>
<ChevronDown
size="md"
fill={t.atoms.text_contrast_low.color}
/>
</View>
</>
)}
</Button>
</Layout.Header.Content>
<Layout.Header.Slot>
<Button
label={_(msg`Pin ${title} to your home screen`)}
size="small"
variant="ghost"
shape="square"
color="secondary">
<ButtonIcon icon={Pin} size="lg" />
</Button>
</Layout.Header.Slot>
</Layout.Header.Outer>
</Layout.Center>
)
}