Lil baby button checkpoint
This commit is contained in:
@@ -21,6 +21,7 @@ export * as Header from '#/components/Layout/Header'
|
|||||||
|
|
||||||
export type ScreenProps = React.ComponentProps<typeof View> & {
|
export type ScreenProps = React.ComponentProps<typeof View> & {
|
||||||
style?: StyleProp<ViewStyle>
|
style?: StyleProp<ViewStyle>
|
||||||
|
noInsetTop?: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -28,6 +29,7 @@ export type ScreenProps = React.ComponentProps<typeof View> & {
|
|||||||
*/
|
*/
|
||||||
export const Screen = React.memo(function Screen({
|
export const Screen = React.memo(function Screen({
|
||||||
style,
|
style,
|
||||||
|
noInsetTop,
|
||||||
...props
|
...props
|
||||||
}: ScreenProps) {
|
}: ScreenProps) {
|
||||||
const {top} = useSafeAreaInsets()
|
const {top} = useSafeAreaInsets()
|
||||||
@@ -35,7 +37,7 @@ export const Screen = React.memo(function Screen({
|
|||||||
<>
|
<>
|
||||||
{isWeb && <WebCenterBorders />}
|
{isWeb && <WebCenterBorders />}
|
||||||
<View
|
<View
|
||||||
style={[a.util_screen_outer, {paddingTop: top}, style]}
|
style={[a.util_screen_outer, {paddingTop: noInsetTop ? 0 : top}, style]}
|
||||||
{...props}
|
{...props}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
import React, {useCallback, useMemo} from 'react'
|
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 {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||||
import {msg, Plural, Trans} from '@lingui/macro'
|
import {msg, Plural, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
import {useIsFocused, useNavigation} from '@react-navigation/native'
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
import {useAnimatedRef} from 'react-native-reanimated'
|
||||||
|
|
||||||
import {HITSLOP_20} from '#/lib/constants'
|
import {HITSLOP_20} from '#/lib/constants'
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
@@ -38,7 +39,6 @@ import {useResolveUriQuery} from '#/state/queries/resolve-uri'
|
|||||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useComposerControls} from '#/state/shell/composer'
|
import {useComposerControls} from '#/state/shell/composer'
|
||||||
import {PagerWithHeader} from '#/view/com/pager/PagerWithHeader'
|
|
||||||
import {PostFeed} from '#/view/com/posts/PostFeed'
|
import {PostFeed} from '#/view/com/posts/PostFeed'
|
||||||
import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
|
import {ProfileSubpageHeader} from '#/view/com/profile/ProfileSubpageHeader'
|
||||||
import {EmptyState} from '#/view/com/util/EmptyState'
|
import {EmptyState} from '#/view/com/util/EmptyState'
|
||||||
@@ -66,7 +66,7 @@ import * as Menu from '#/components/Menu'
|
|||||||
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
import {ReportDialog, useReportDialogControl} from '#/components/ReportDialog'
|
||||||
import {RichText} from '#/components/RichText'
|
import {RichText} from '#/components/RichText'
|
||||||
|
|
||||||
const SECTION_TITLES = ['Posts']
|
import {ProfileSubpageHeader as NewHeader} from '#/screens/Profile/components/ProfileSubpageHeader'
|
||||||
|
|
||||||
interface SectionRef {
|
interface SectionRef {
|
||||||
scrollToTop: () => void
|
scrollToTop: () => void
|
||||||
@@ -125,8 +125,10 @@ export function ProfileFeedScreen(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return resolvedUri ? (
|
return resolvedUri ? (
|
||||||
<Layout.Screen>
|
<Layout.Screen noInsetTop>
|
||||||
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
|
<Layout.Center>
|
||||||
|
<ProfileFeedScreenIntermediate feedUri={resolvedUri.uri} />
|
||||||
|
</Layout.Center>
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
) : (
|
) : (
|
||||||
<Layout.Screen>
|
<Layout.Screen>
|
||||||
@@ -164,7 +166,6 @@ export function ProfileFeedScreenInner({
|
|||||||
const reportDialogControl = useReportDialogControl()
|
const reportDialogControl = useReportDialogControl()
|
||||||
const {openComposer} = useComposerControls()
|
const {openComposer} = useComposerControls()
|
||||||
const playHaptic = useHaptics()
|
const playHaptic = useHaptics()
|
||||||
const feedSectionRef = React.useRef<SectionRef>(null)
|
|
||||||
const isScreenFocused = useIsFocused()
|
const isScreenFocused = useIsFocused()
|
||||||
|
|
||||||
const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
|
const {mutateAsync: addSavedFeeds, isPending: isAddSavedFeedPending} =
|
||||||
@@ -257,18 +258,9 @@ export function ProfileFeedScreenInner({
|
|||||||
reportDialogControl.open()
|
reportDialogControl.open()
|
||||||
}, [reportDialogControl])
|
}, [reportDialogControl])
|
||||||
|
|
||||||
const onCurrentPageSelected = React.useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
if (index === 0) {
|
|
||||||
feedSectionRef.current?.scrollToTop()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[feedSectionRef],
|
|
||||||
)
|
|
||||||
|
|
||||||
const renderHeader = useCallback(() => {
|
const renderHeader = useCallback(() => {
|
||||||
return (
|
return (
|
||||||
<>
|
<View>
|
||||||
<ProfileSubpageHeader
|
<ProfileSubpageHeader
|
||||||
isLoading={false}
|
isLoading={false}
|
||||||
href={feedInfo.route.href}
|
href={feedInfo.route.href}
|
||||||
@@ -370,12 +362,13 @@ export function ProfileFeedScreenInner({
|
|||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
</View>
|
</View>
|
||||||
</ProfileSubpageHeader>
|
</ProfileSubpageHeader>
|
||||||
|
|
||||||
<AboutSection
|
<AboutSection
|
||||||
feedOwnerDid={feedInfo.creatorDid}
|
feedOwnerDid={feedInfo.creatorDid}
|
||||||
feedRkey={feedInfo.route.params.rkey}
|
feedRkey={feedInfo.route.params.rkey}
|
||||||
feedInfo={feedInfo}
|
feedInfo={feedInfo}
|
||||||
/>
|
/>
|
||||||
</>
|
</View>
|
||||||
)
|
)
|
||||||
}, [
|
}, [
|
||||||
_,
|
_,
|
||||||
@@ -392,6 +385,34 @@ export function ProfileFeedScreenInner({
|
|||||||
isPending,
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<ReportDialog
|
<ReportDialog
|
||||||
@@ -402,21 +423,35 @@ export function ProfileFeedScreenInner({
|
|||||||
cid: feedInfo.cid,
|
cid: feedInfo.cid,
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<PagerWithHeader
|
|
||||||
items={SECTION_TITLES}
|
<NewHeader
|
||||||
isHeaderReady={true}
|
title={feedInfo.displayName}
|
||||||
renderHeader={renderHeader}
|
avatar={feedInfo.avatar}
|
||||||
onCurrentPageSelected={onCurrentPageSelected}>
|
creator={{did: feedInfo.creatorDid, handle: feedInfo.creatorHandle}}
|
||||||
{({headerHeight, scrollElRef, isFocused}) => (
|
likeCount={feedInfo.likeCount || 0}
|
||||||
<FeedSection
|
/>
|
||||||
ref={feedSectionRef}
|
|
||||||
feed={`feedgen|${feedInfo.uri}`}
|
<FeedFeedbackProvider value={feedFeedback}>
|
||||||
headerHeight={headerHeight}
|
<PostFeed
|
||||||
scrollElRef={scrollElRef as ListRef}
|
feed={feed}
|
||||||
isFocused={isScreenFocused && isFocused}
|
pollInterval={60e3}
|
||||||
/>
|
disablePoll={hasNew}
|
||||||
)}
|
onHasNew={setHasNew}
|
||||||
</PagerWithHeader>
|
scrollElRef={scrollElRef}
|
||||||
|
onScrolledDownChange={setIsScrolledDown}
|
||||||
|
renderEmptyState={renderPostsEmpty}
|
||||||
|
ListHeaderComponent={renderHeader}
|
||||||
|
/>
|
||||||
|
</FeedFeedbackProvider>
|
||||||
|
|
||||||
|
{(isScrolledDown || hasNew) && (
|
||||||
|
<LoadLatestBtn
|
||||||
|
onPress={onScrollToTop}
|
||||||
|
label={_(msg`Load new posts`)}
|
||||||
|
showIndicator={hasNew}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{hasSession && (
|
{hasSession && (
|
||||||
<FAB
|
<FAB
|
||||||
testID="composeFAB"
|
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({
|
function AboutSection({
|
||||||
feedOwnerDid,
|
feedOwnerDid,
|
||||||
feedRkey,
|
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>
|
||||||
|
)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user