Adjust see more posts behavior again
This commit is contained in:
@@ -4,15 +4,16 @@ import Animated, {
|
|||||||
FadeInDown,
|
FadeInDown,
|
||||||
FadeOut,
|
FadeOut,
|
||||||
interpolate,
|
interpolate,
|
||||||
|
type SharedValue,
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
} from 'react-native-reanimated'
|
} from 'react-native-reanimated'
|
||||||
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
||||||
import {scheduleOnRN} from 'react-native-worklets'
|
import {scheduleOnRN} from 'react-native-worklets'
|
||||||
import {Trans, useLingui} from '@lingui/react/macro'
|
import {plural} from '@lingui/core/macro'
|
||||||
|
import {Plural, Trans, useLingui} from '@lingui/react/macro'
|
||||||
|
|
||||||
import {useHaptics} from '#/lib/haptics'
|
import {useHaptics} from '#/lib/haptics'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {useHomeHeaderMode} from '#/view/com/util/MainScrollProvider'
|
|
||||||
import {atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useLayoutBreakpoints, useTheme} from '#/alf'
|
||||||
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
import {useInteractionState} from '#/components/hooks/useInteractionState'
|
||||||
import {ArrowTop_Stroke2_Corner0_Rounded as ArrowUpIcon} from '#/components/icons/Arrow'
|
import {ArrowTop_Stroke2_Corner0_Rounded as ArrowUpIcon} from '#/components/icons/Arrow'
|
||||||
@@ -30,14 +31,33 @@ const AnimatedPressable = Animated.createAnimatedComponent(Pressable)
|
|||||||
*/
|
*/
|
||||||
export function SeeNewPostsPill({
|
export function SeeNewPostsPill({
|
||||||
onPress: onPressInner,
|
onPress: onPressInner,
|
||||||
|
count = 0,
|
||||||
topOffset = 0,
|
topOffset = 0,
|
||||||
|
headerMode,
|
||||||
|
attached = false,
|
||||||
}: {
|
}: {
|
||||||
onPress: () => void
|
onPress: () => void
|
||||||
|
/**
|
||||||
|
* Number of new posts above the user. Zero (unknown) falls back to a
|
||||||
|
* generic label.
|
||||||
|
*/
|
||||||
|
count?: number
|
||||||
/**
|
/**
|
||||||
* Height of the floating feed header the pill must clear. Pass the same
|
* Height of the floating feed header the pill must clear. Pass the same
|
||||||
* headerOffset used by the feed list.
|
* headerOffset used by the feed list.
|
||||||
*/
|
*/
|
||||||
topOffset?: number
|
topOffset?: number
|
||||||
|
/**
|
||||||
|
* The home header's minimal-shell mode, passed in as a prop because the
|
||||||
|
* pill renders in a Portal outside HomeHeaderModeProvider.
|
||||||
|
*/
|
||||||
|
headerMode: SharedValue<number>
|
||||||
|
/**
|
||||||
|
* When set, the pill is rendered inside the home header (via
|
||||||
|
* HomeHeaderPortal) and anchors itself directly below the sticky tab bar
|
||||||
|
* instead of using fixed viewport positioning. Larger web layouts only.
|
||||||
|
*/
|
||||||
|
attached?: boolean
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
const t = useTheme()
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
@@ -57,7 +77,6 @@ export function SeeNewPostsPill({
|
|||||||
*/
|
*/
|
||||||
const top = topOffset > 0 ? topOffset + 12 : gtMobile ? 64 : 12
|
const top = topOffset > 0 ? topOffset + 12 : gtMobile ? 64 : 12
|
||||||
|
|
||||||
const headerMode = useHomeHeaderMode()
|
|
||||||
const {headerHeight} = useShellLayout()
|
const {headerHeight} = useShellLayout()
|
||||||
const {top: topInset} = useSafeAreaInsets()
|
const {top: topInset} = useSafeAreaInsets()
|
||||||
const headerPinnedHeight = IS_LIQUID_GLASS ? topInset : 0
|
const headerPinnedHeight = IS_LIQUID_GLASS ? topInset : 0
|
||||||
@@ -92,16 +111,26 @@ export function SeeNewPostsPill({
|
|||||||
style={[
|
style={[
|
||||||
a.z_20,
|
a.z_20,
|
||||||
/*
|
/*
|
||||||
* On web the pill is fixed and must be centered on the content
|
* Attached mode anchors the pill directly below its parent (the
|
||||||
* column, not the viewport - the column shifts at some widths (nav
|
* sticky tab bar), which is already column-width, so centering is
|
||||||
* rail, tablet offset), so mirror Layout's WebCenterBorders
|
* trivial. Otherwise, on web the pill is fixed and must be centered
|
||||||
* centering. On native the feed spans the screen, so a full-width
|
* on the content column, not the viewport - the column shifts at
|
||||||
* absolute container can simply center its child.
|
* some widths (nav rail, tablet offset), so mirror Layout's
|
||||||
|
* WebCenterBorders centering. On native the feed spans the screen,
|
||||||
|
* so a full-width absolute container can simply center its child.
|
||||||
*/
|
*/
|
||||||
IS_WEB
|
attached
|
||||||
|
? [
|
||||||
|
a.absolute,
|
||||||
|
a.w_full,
|
||||||
|
a.align_center,
|
||||||
|
{top: '100%' as const, paddingTop: 12},
|
||||||
|
]
|
||||||
|
: IS_WEB
|
||||||
? [
|
? [
|
||||||
a.fixed,
|
a.fixed,
|
||||||
{
|
{
|
||||||
|
top,
|
||||||
left: '50%',
|
left: '50%',
|
||||||
transform: [
|
transform: [
|
||||||
{translateX: '-50%'},
|
{translateX: '-50%'},
|
||||||
@@ -112,9 +141,8 @@ export function SeeNewPostsPill({
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [a.absolute, a.w_full, a.align_center],
|
: [a.absolute, a.w_full, a.align_center, {top}],
|
||||||
{
|
{
|
||||||
top,
|
|
||||||
// Don't prevent scrolling in this area _except_ for in the pill itself
|
// Don't prevent scrolling in this area _except_ for in the pill itself
|
||||||
pointerEvents: 'box-none',
|
pointerEvents: 'box-none',
|
||||||
},
|
},
|
||||||
@@ -128,7 +156,11 @@ export function SeeNewPostsPill({
|
|||||||
<AnimatedPressable
|
<AnimatedPressable
|
||||||
testID="seeNewPostsPill"
|
testID="seeNewPostsPill"
|
||||||
accessibilityRole="button"
|
accessibilityRole="button"
|
||||||
accessibilityLabel={l`See new posts`}
|
accessibilityLabel={
|
||||||
|
count > 0
|
||||||
|
? plural(count, {one: '# new post', other: '# new posts'})
|
||||||
|
: l`See new posts`
|
||||||
|
}
|
||||||
accessibilityHint={l`Scrolls to the top of the feed`}
|
accessibilityHint={l`Scrolls to the top of the feed`}
|
||||||
style={[
|
style={[
|
||||||
a.flex_row,
|
a.flex_row,
|
||||||
@@ -153,11 +185,15 @@ export function SeeNewPostsPill({
|
|||||||
onPointerLeave={onHoverOut}>
|
onPointerLeave={onHoverOut}>
|
||||||
<SubtleHover hover={hovered} style={[a.rounded_full]} />
|
<SubtleHover hover={hovered} style={[a.rounded_full]} />
|
||||||
<ArrowUpIcon
|
<ArrowUpIcon
|
||||||
size="xs"
|
size="sm"
|
||||||
style={[a.z_10, {color: t.palette.primary_600}]}
|
style={[a.z_10, {color: t.palette.primary_600}]}
|
||||||
/>
|
/>
|
||||||
<Text style={[a.z_10, a.font_bold, {color: t.palette.primary_600}]}>
|
<Text style={[a.z_10, a.font_bold, {color: t.palette.primary_600}]}>
|
||||||
|
{count > 0 ? (
|
||||||
|
<Plural value={count} one="# new post" other="# new posts" />
|
||||||
|
) : (
|
||||||
<Trans>See new posts</Trans>
|
<Trans>See new posts</Trans>
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
</AnimatedPressable>
|
</AnimatedPressable>
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
|
|||||||
@@ -416,6 +416,44 @@ export function usePostFeedQuery(
|
|||||||
return query
|
return query
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Like pollLatest, but returns how many new posts would appear above the
|
||||||
|
* current head of the feed, capped at the size of a single fetch. Used by
|
||||||
|
* the Following feed's "N new posts" pill.
|
||||||
|
*/
|
||||||
|
export async function pollLatestCount(
|
||||||
|
page: FeedPage | undefined,
|
||||||
|
): Promise<number> {
|
||||||
|
if (!page) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if (AppState.currentState !== 'active') {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const headUri = page.slices[0]?.items[0]?.uri
|
||||||
|
if (!headUri) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug('usePostFeedQuery: pollLatestCount')
|
||||||
|
const res = await page.api.fetch({cursor: undefined, limit: 30})
|
||||||
|
const newPosts = []
|
||||||
|
for (const post of res.feed) {
|
||||||
|
if (post.post.uri === headUri) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
newPosts.push(post)
|
||||||
|
}
|
||||||
|
if (newPosts.length === 0) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
const slices = page.tuner.tune(newPosts, {
|
||||||
|
dryRun: true,
|
||||||
|
})
|
||||||
|
return slices.reduce((count, slice) => count + slice.items.length, 0)
|
||||||
|
}
|
||||||
|
|
||||||
export async function pollLatest(page: FeedPage | undefined) {
|
export async function pollLatest(page: FeedPage | undefined) {
|
||||||
if (!page) {
|
if (!page) {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import {View} from 'react-native'
|
|||||||
import {type AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
|
import {type AppBskyActorDefs, AppBskyFeedDefs} from '@atproto/api'
|
||||||
import {msg} from '@lingui/core/macro'
|
import {msg} from '@lingui/core/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {type NavigationProp, useNavigation} from '@react-navigation/native'
|
import {
|
||||||
|
type NavigationProp,
|
||||||
|
useIsFocused,
|
||||||
|
useNavigation,
|
||||||
|
} from '@react-navigation/native'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
import {useQueryClient} from '@tanstack/react-query'
|
||||||
|
|
||||||
import {DISCOVER_FEED_URI, VIDEO_FEED_URIS} from '#/lib/constants'
|
import {DISCOVER_FEED_URI, VIDEO_FEED_URIS} from '#/lib/constants'
|
||||||
@@ -29,17 +33,22 @@ import {
|
|||||||
} from '#/state/queries/post-feed'
|
} from '#/state/queries/post-feed'
|
||||||
import {truncateAndInvalidate} from '#/state/queries/util'
|
import {truncateAndInvalidate} from '#/state/queries/util'
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
|
import {Portal as HomeHeaderPortal} from '#/view/com/home/HomeHeaderPortal'
|
||||||
import {PostFeed} from '#/view/com/posts/PostFeed'
|
import {PostFeed} from '#/view/com/posts/PostFeed'
|
||||||
import {FAB} from '#/view/com/util/fab/FAB'
|
import {FAB} from '#/view/com/util/fab/FAB'
|
||||||
import {type ListMethods} from '#/view/com/util/List'
|
import {type ListMethods} from '#/view/com/util/List'
|
||||||
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
import {LoadLatestBtn} from '#/view/com/util/load-latest/LoadLatestBtn'
|
||||||
import {MainScrollProvider} from '#/view/com/util/MainScrollProvider'
|
import {
|
||||||
import {useTheme} from '#/alf'
|
MainScrollProvider,
|
||||||
|
useHomeHeaderMode,
|
||||||
|
} from '#/view/com/util/MainScrollProvider'
|
||||||
|
import {useBreakpoints, useTheme} from '#/alf'
|
||||||
import {SeeNewPostsPill} from '#/components/feeds/SeeNewPostsPill'
|
import {SeeNewPostsPill} from '#/components/feeds/SeeNewPostsPill'
|
||||||
import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
|
import {useHeaderOffset} from '#/components/hooks/useHeaderOffset'
|
||||||
import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig'
|
import {EditBig_Stroke2_Corner2_Rounded as EditBigIcon} from '#/components/icons/EditBig'
|
||||||
|
import {Portal} from '#/components/Portal'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE, IS_WEB} from '#/env'
|
||||||
|
|
||||||
const POLL_FREQ = 60e3 // 60sec
|
const POLL_FREQ = 60e3 // 60sec
|
||||||
|
|
||||||
@@ -66,6 +75,9 @@ export function FeedPage({
|
|||||||
}) {
|
}) {
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const {hasSession, currentAccount} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
|
const headerMode = useHomeHeaderMode()
|
||||||
|
const isScreenFocused = useIsFocused()
|
||||||
|
const {gtMobile} = useBreakpoints()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp<AllNavigatorParams>>()
|
const navigation = useNavigation<NavigationProp<AllNavigatorParams>>()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -81,6 +93,11 @@ export function FeedPage({
|
|||||||
* by pressing the pill or by scrolling up on their own.
|
* by pressing the pill or by scrolling up on their own.
|
||||||
*/
|
*/
|
||||||
const [showResumePill, setShowResumePill] = useState(false)
|
const [showResumePill, setShowResumePill] = useState(false)
|
||||||
|
/**
|
||||||
|
* How many unseen posts are above the user, from either the restore path
|
||||||
|
* or the new-posts poll. Shown in the pill.
|
||||||
|
*/
|
||||||
|
const [newPostsCount, setNewPostsCount] = useState(0)
|
||||||
const wasScrolledDownRef = useRef(false)
|
const wasScrolledDownRef = useRef(false)
|
||||||
const setHomeBadge = useSetHomeBadge()
|
const setHomeBadge = useSetHomeBadge()
|
||||||
const isVideoFeed = useMemo(() => {
|
const isVideoFeed = useMemo(() => {
|
||||||
@@ -118,8 +135,9 @@ export function FeedPage({
|
|||||||
}
|
}
|
||||||
}, [isScrolledDown])
|
}, [isScrolledDown])
|
||||||
|
|
||||||
const onPositionRestored = useCallback(() => {
|
const onPositionRestored = useCallback((unseenCount: number) => {
|
||||||
setShowResumePill(true)
|
setShowResumePill(true)
|
||||||
|
setNewPostsCount(unseenCount)
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
const onSoftReset = useCallback(() => {
|
const onSoftReset = useCallback(() => {
|
||||||
@@ -204,9 +222,17 @@ export function FeedPage({
|
|||||||
* reaching the top fulfills it and the pill hides. When hasNew is set the
|
* reaching the top fulfills it and the pill hides. When hasNew is set the
|
||||||
* posts are not loaded yet, so the pill persists at any scroll position
|
* posts are not loaded yet, so the pill persists at any scroll position
|
||||||
* until it is pressed or the feed is refreshed.
|
* until it is pressed or the feed is refreshed.
|
||||||
|
*
|
||||||
|
* Gated on isPageFocused (the pager tab) and isScreenFocused (the Home
|
||||||
|
* screen itself) because the pill renders in a Portal outside this
|
||||||
|
* page's subtree, so it would otherwise stay visible on other feed tabs
|
||||||
|
* and on entirely different screens like Notifications.
|
||||||
*/
|
*/
|
||||||
const showSeeNewPostsPill =
|
const showSeeNewPostsPill =
|
||||||
isFollowingFeed && (hasNew || (showResumePill && isScrolledDown))
|
isFollowingFeed &&
|
||||||
|
isPageFocused &&
|
||||||
|
isScreenFocused &&
|
||||||
|
(hasNew || (showResumePill && isScrolledDown))
|
||||||
return (
|
return (
|
||||||
<View
|
<View
|
||||||
testID={testID}
|
testID={testID}
|
||||||
@@ -220,10 +246,15 @@ export function FeedPage({
|
|||||||
feed={feed}
|
feed={feed}
|
||||||
feedParams={feedParams}
|
feedParams={feedParams}
|
||||||
pollInterval={POLL_FREQ}
|
pollInterval={POLL_FREQ}
|
||||||
disablePoll={hasNew || !isPageFocused}
|
/*
|
||||||
|
* On the Following feed, keep polling after hasNew so the pill's
|
||||||
|
* new-post count stays up to date.
|
||||||
|
*/
|
||||||
|
disablePoll={!isPageFocused || (hasNew && !isFollowingFeed)}
|
||||||
scrollElRef={scrollElRef}
|
scrollElRef={scrollElRef}
|
||||||
onScrolledDownChange={setIsScrolledDown}
|
onScrolledDownChange={setIsScrolledDown}
|
||||||
onHasNew={setHasNew}
|
onHasNew={setHasNew}
|
||||||
|
onHasNewCount={isFollowingFeed ? setNewPostsCount : undefined}
|
||||||
onPositionRestored={onPositionRestored}
|
onPositionRestored={onPositionRestored}
|
||||||
renderEmptyState={renderEmptyState}
|
renderEmptyState={renderEmptyState}
|
||||||
renderEndOfFeed={renderEndOfFeed}
|
renderEndOfFeed={renderEndOfFeed}
|
||||||
@@ -233,12 +264,30 @@ export function FeedPage({
|
|||||||
/>
|
/>
|
||||||
</FeedFeedbackProvider>
|
</FeedFeedbackProvider>
|
||||||
</MainScrollProvider>
|
</MainScrollProvider>
|
||||||
{showSeeNewPostsPill && (
|
{showSeeNewPostsPill &&
|
||||||
|
(IS_WEB && gtMobile ? (
|
||||||
|
/*
|
||||||
|
* Larger web layouts anchor the pill inside the home header,
|
||||||
|
* directly below the sticky tab bar.
|
||||||
|
*/
|
||||||
|
<HomeHeaderPortal>
|
||||||
<SeeNewPostsPill
|
<SeeNewPostsPill
|
||||||
|
attached
|
||||||
|
count={newPostsCount}
|
||||||
|
onPress={onPressSeeNewPosts}
|
||||||
|
headerMode={headerMode}
|
||||||
|
/>
|
||||||
|
</HomeHeaderPortal>
|
||||||
|
) : (
|
||||||
|
<Portal>
|
||||||
|
<SeeNewPostsPill
|
||||||
|
count={newPostsCount}
|
||||||
onPress={onPressSeeNewPosts}
|
onPress={onPressSeeNewPosts}
|
||||||
topOffset={headerOffset}
|
topOffset={headerOffset}
|
||||||
|
headerMode={headerMode}
|
||||||
/>
|
/>
|
||||||
)}
|
</Portal>
|
||||||
|
))}
|
||||||
{(isScrolledDown || (hasNew && !isFollowingFeed)) && (
|
{(isScrolledDown || (hasNew && !isFollowingFeed)) && (
|
||||||
<LoadLatestBtn
|
<LoadLatestBtn
|
||||||
onPress={onPressLoadLatest}
|
onPress={onPressLoadLatest}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {HITSLOP_10} from '#/lib/constants'
|
|||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {useShellLayout} from '#/state/shell/shell-layout'
|
import {useShellLayout} from '#/state/shell/shell-layout'
|
||||||
import {HomeHeaderLayoutMobile} from '#/view/com/home/HomeHeaderLayoutMobile'
|
import {HomeHeaderLayoutMobile} from '#/view/com/home/HomeHeaderLayoutMobile'
|
||||||
|
import {Outlet as HomeHeaderPortalOutlet} from '#/view/com/home/HomeHeaderPortal'
|
||||||
import {Logo} from '#/view/icons/Logo'
|
import {Logo} from '#/view/icons/Logo'
|
||||||
import {useLogoVariant} from '#/view/icons/useLogoVariant'
|
import {useLogoVariant} from '#/view/icons/useLogoVariant'
|
||||||
import {atoms as a, useBreakpoints, useGutters, useTheme} from '#/alf'
|
import {atoms as a, useBreakpoints, useGutters, useTheme} from '#/alf'
|
||||||
@@ -85,6 +86,7 @@ function HomeHeaderLayoutDesktopAndTablet({
|
|||||||
headerHeight.set(e.nativeEvent.layout.height)
|
headerHeight.set(e.nativeEvent.layout.height)
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
|
<HomeHeaderPortalOutlet />
|
||||||
</Layout.Center>
|
</Layout.Center>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import {createPortalGroup} from '#/components/Portal'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders into the home header on larger web layouts, directly below the
|
||||||
|
* sticky tab bar. Lets the Following feed pin its "N new posts" pill under
|
||||||
|
* the bar regardless of scroll position.
|
||||||
|
*/
|
||||||
|
const group = createPortalGroup()
|
||||||
|
|
||||||
|
export const Provider = group.Provider
|
||||||
|
export const Outlet = group.Outlet
|
||||||
|
export const Portal = group.Portal
|
||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
type FeedPostSlice,
|
type FeedPostSlice,
|
||||||
type FeedPostSliceItem,
|
type FeedPostSliceItem,
|
||||||
pollLatest,
|
pollLatest,
|
||||||
|
pollLatestCount,
|
||||||
RQKEY,
|
RQKEY,
|
||||||
usePostFeedQuery,
|
usePostFeedQuery,
|
||||||
} from '#/state/queries/post-feed'
|
} from '#/state/queries/post-feed'
|
||||||
@@ -248,6 +249,7 @@ let PostFeed = ({
|
|||||||
scrollElRef,
|
scrollElRef,
|
||||||
onScrolledDownChange,
|
onScrolledDownChange,
|
||||||
onHasNew,
|
onHasNew,
|
||||||
|
onHasNewCount,
|
||||||
onPositionRestored,
|
onPositionRestored,
|
||||||
renderEmptyState,
|
renderEmptyState,
|
||||||
renderEndOfFeed,
|
renderEndOfFeed,
|
||||||
@@ -272,12 +274,18 @@ let PostFeed = ({
|
|||||||
disablePoll?: boolean
|
disablePoll?: boolean
|
||||||
scrollElRef?: ListRef
|
scrollElRef?: ListRef
|
||||||
onHasNew?: (v: boolean) => void
|
onHasNew?: (v: boolean) => void
|
||||||
|
/**
|
||||||
|
* When provided, the new-posts poll also reports how many posts would
|
||||||
|
* appear above the current head of the feed (capped at one fetch).
|
||||||
|
*/
|
||||||
|
onHasNewCount?: (count: number) => void
|
||||||
onScrolledDownChange?: (isScrolledDown: boolean) => void
|
onScrolledDownChange?: (isScrolledDown: boolean) => void
|
||||||
/**
|
/**
|
||||||
* Called after the feed successfully scrolled back to the last read
|
* Called after the feed successfully scrolled back to the last read
|
||||||
* position on cold start.
|
* position on cold start and there are posts above that the user has not
|
||||||
|
* seen, with the number of unseen posts.
|
||||||
*/
|
*/
|
||||||
onPositionRestored?: () => void
|
onPositionRestored?: (unseenCount: number) => void
|
||||||
renderEmptyState: () => React.ReactElement
|
renderEmptyState: () => React.ReactElement
|
||||||
renderEndOfFeed?: () => React.ReactElement
|
renderEndOfFeed?: () => React.ReactElement
|
||||||
testID?: string
|
testID?: string
|
||||||
@@ -369,7 +377,17 @@ let PostFeed = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (await pollLatest(data.pages[0])) {
|
if (onHasNewCount) {
|
||||||
|
const count = await pollLatestCount(data.pages[0])
|
||||||
|
if (count > 0) {
|
||||||
|
if (isEmpty) {
|
||||||
|
void refetch()
|
||||||
|
} else {
|
||||||
|
onHasNew(true)
|
||||||
|
onHasNewCount(count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (await pollLatest(data.pages[0])) {
|
||||||
if (isEmpty) {
|
if (isEmpty) {
|
||||||
void refetch()
|
void refetch()
|
||||||
} else {
|
} else {
|
||||||
@@ -847,13 +865,21 @@ let PostFeed = ({
|
|||||||
pagesFetched: data.pages.length,
|
pagesFetched: data.pages.length,
|
||||||
})
|
})
|
||||||
/*
|
/*
|
||||||
* Only announce the restore (which surfaces the "See new posts" pill)
|
* Only announce the restore (which surfaces the "N new posts" pill)
|
||||||
* if the feed's head post is one the user hasn't seen - otherwise
|
* if there are posts above that the user hasn't seen - i.e. rows
|
||||||
* everything above the anchor was already read.
|
* between the fresh head and the newest post they'd seen last time.
|
||||||
*/
|
*/
|
||||||
const headUri = getHeadUri(feedItems)
|
let unseenCount = 0
|
||||||
if (headUri && headUri !== seenHeadUriRef.current) {
|
for (const row of feedItems.slice(0, index)) {
|
||||||
onPositionRestored?.()
|
if (row.type !== 'sliceItem') continue
|
||||||
|
const uri = row.slice.items[row.indexInSlice].uri
|
||||||
|
if (uri === seenHeadUriRef.current) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
unseenCount++
|
||||||
|
}
|
||||||
|
if (unseenCount > 0) {
|
||||||
|
onPositionRestored?.(unseenCount)
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
isError ||
|
isError ||
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {useLoggedOutViewControls} from '#/state/shell/logged-out'
|
|||||||
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
|
import {useSelectedFeed, useSetSelectedFeed} from '#/state/shell/selected-feed'
|
||||||
import {FeedPage} from '#/view/com/feeds/FeedPage'
|
import {FeedPage} from '#/view/com/feeds/FeedPage'
|
||||||
import {HomeHeader} from '#/view/com/home/HomeHeader'
|
import {HomeHeader} from '#/view/com/home/HomeHeader'
|
||||||
|
import {Provider as HomeHeaderPortalProvider} from '#/view/com/home/HomeHeaderPortal'
|
||||||
import {
|
import {
|
||||||
Pager,
|
Pager,
|
||||||
type PagerRef,
|
type PagerRef,
|
||||||
@@ -88,11 +89,13 @@ export function HomeScreen(props: Props) {
|
|||||||
return (
|
return (
|
||||||
<Layout.Screen testID="HomeScreen" noInsetTop={IS_LIQUID_GLASS}>
|
<Layout.Screen testID="HomeScreen" noInsetTop={IS_LIQUID_GLASS}>
|
||||||
<HomeHeaderModeProvider>
|
<HomeHeaderModeProvider>
|
||||||
|
<HomeHeaderPortalProvider>
|
||||||
<HomeScreenReady
|
<HomeScreenReady
|
||||||
{...props}
|
{...props}
|
||||||
preferences={preferences}
|
preferences={preferences}
|
||||||
pinnedFeedInfos={pinnedFeedInfos}
|
pinnedFeedInfos={pinnedFeedInfos}
|
||||||
/>
|
/>
|
||||||
|
</HomeHeaderPortalProvider>
|
||||||
</HomeHeaderModeProvider>
|
</HomeHeaderModeProvider>
|
||||||
</Layout.Screen>
|
</Layout.Screen>
|
||||||
)
|
)
|
||||||
|
|||||||
Reference in New Issue
Block a user