Skip to content

Commit

Permalink
Lil baby button checkpoint
Browse files Browse the repository at this point in the history
  • Loading branch information
estrattonbailey committed Dec 11, 2024
1 parent d4efa91 commit 4d380a0
Show file tree
Hide file tree
Showing 3 changed files with 195 additions and 100 deletions.
4 changes: 3 additions & 1 deletion src/components/Layout/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,23 @@ export * as Header from '#/components/Layout/Header'

export type ScreenProps = React.ComponentProps<typeof View> & {
style?: StyleProp<ViewStyle>
noInsetTop?: boolean
}

/**
* Outermost component of every screen
*/
export const Screen = React.memo(function Screen({
style,
noInsetTop,
...props
}: ScreenProps) {
const {top} = useSafeAreaInsets()
return (
<>
{isWeb && <WebCenterBorders />}
<View
style={[a.util_screen_outer, {paddingTop: top}, style]}
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
{...props}
/>
</>
Expand Down
166 changes: 67 additions & 99 deletions src/screens/Profile/ProfileFeed/index.tsx
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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'
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -125,8 +125,10 @@ export function ProfileFeedScreen(props: Props) {
}

return resolvedUri ? (
<Layout.Screen>
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
<Layout.Screen noInsetTop>
<Layout.Center>
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
</Layout.Center>
</Layout.Screen>
) : (
<Layout.Screen>
Expand Down Expand Up @@ -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} =
Expand Down Expand Up @@ -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}
Expand Down Expand Up @@ -370,12 +362,13 @@ export function ProfileFeedScreenInner({
</Menu.Root>
</View>
</ProfileSubpageHeader>

<AboutSection
feedOwnerDid={feedInfo.creatorDid}
feedRkey={feedInfo.route.params.rkey}
feedInfo={feedInfo}
/>
</>
</View>
)
}, [
_,
Expand All @@ -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
Expand All @@ -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}
/>
)}
</PagerWithHeader>

<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}
/>
)}

{hasSession && (
<FAB
testID="composeFAB"
Expand All @@ -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,
Expand Down
125 changes: 125 additions & 0 deletions src/screens/Profile/components/ProfileSubpageHeader.tsx
Original file line number Diff line number Diff line change
@@ -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>
)
}

0 comments on commit 4d380a0

Please sign in to comment.