Prototype: remember your place in following feed
This commit is contained in:
@@ -93,6 +93,23 @@ export function getFeatureDescription(feature: Features, i18n: I18n) {
|
|||||||
}),
|
}),
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
case Features.FollowingFeedResumeEnable:
|
||||||
|
return {
|
||||||
|
key: feature,
|
||||||
|
name: i18n._(
|
||||||
|
msg({
|
||||||
|
message: 'Remember your place',
|
||||||
|
comment:
|
||||||
|
'Name for a feature flag (Reopen the Following feed at the last post you read.)',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
description: i18n._(
|
||||||
|
msg({
|
||||||
|
message: 'Reopen the Following feed at the last post you read.',
|
||||||
|
comment: 'Description of a feature flag (Remember your place)',
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
}
|
||||||
case Features.CanonicalPostNumberingEnable:
|
case Features.CanonicalPostNumberingEnable:
|
||||||
return {
|
return {
|
||||||
key: feature,
|
key: feature,
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ export enum Features {
|
|||||||
FollowSortEnable = 'follow_sort:enable',
|
FollowSortEnable = 'follow_sort:enable',
|
||||||
OnboardingInterestsRequiredEnable = 'onboarding:interests:required:enable',
|
OnboardingInterestsRequiredEnable = 'onboarding:interests:required:enable',
|
||||||
CanonicalPostNumberingEnable = 'canonical_post_numbering:enable',
|
CanonicalPostNumberingEnable = 'canonical_post_numbering:enable',
|
||||||
|
FollowingFeedResumeEnable = 'following_feed:resume:enable',
|
||||||
|
|
||||||
// values
|
// values
|
||||||
TrendingDiscoverValues = 'trending_discover:values',
|
TrendingDiscoverValues = 'trending_discover:values',
|
||||||
|
|||||||
@@ -198,6 +198,21 @@ export type Events = {
|
|||||||
feedType: string
|
feedType: string
|
||||||
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
|
reason: 'pull-to-refresh' | 'soft-reset' | 'load-latest'
|
||||||
}
|
}
|
||||||
|
'feed:resume:toggle': {
|
||||||
|
/** The state the user switched the setting to. */
|
||||||
|
enabled: boolean
|
||||||
|
}
|
||||||
|
'feed:positionRestored': {
|
||||||
|
/**
|
||||||
|
* `restored` means we scrolled to the anchor post; `not-found` means the
|
||||||
|
* anchor never showed up within the page cap; `aborted` means the user
|
||||||
|
* scrolled or refreshed before the restore could complete.
|
||||||
|
*/
|
||||||
|
outcome: 'restored' | 'not-found' | 'aborted'
|
||||||
|
/** Number of rows above the restored anchor, when restored. */
|
||||||
|
depth?: number
|
||||||
|
pagesFetched: number
|
||||||
|
}
|
||||||
'feed:save': {
|
'feed:save': {
|
||||||
feedUrl: string
|
feedUrl: string
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
type CommonNavigatorParams,
|
type CommonNavigatorParams,
|
||||||
type NativeStackScreenProps,
|
type NativeStackScreenProps,
|
||||||
} from '#/lib/routes/types'
|
} from '#/lib/routes/types'
|
||||||
|
import {useFollowingFeedResumeEnabled} from '#/state/feed-position'
|
||||||
import {
|
import {
|
||||||
usePreferencesQuery,
|
usePreferencesQuery,
|
||||||
useSetFeedViewPreferencesMutation,
|
useSetFeedViewPreferencesMutation,
|
||||||
@@ -15,9 +16,11 @@ import {Admonition} from '#/components/Admonition'
|
|||||||
import * as Toggle from '#/components/forms/Toggle'
|
import * as Toggle from '#/components/forms/Toggle'
|
||||||
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
import {Beaker_Stroke2_Corner2_Rounded as BeakerIcon} from '#/components/icons/Beaker'
|
||||||
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
|
import {Bubbles_Stroke2_Corner2_Rounded as BubblesIcon} from '#/components/icons/Bubble'
|
||||||
|
import {Clock_Stroke2_Corner0_Rounded as ClockIcon} from '#/components/icons/Clock'
|
||||||
import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote'
|
import {CloseQuote_Stroke2_Corner1_Rounded as QuoteIcon} from '#/components/icons/Quote'
|
||||||
import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
|
import {Repost_Stroke2_Corner2_Rounded as RepostIcon} from '#/components/icons/Repost'
|
||||||
import * as Layout from '#/components/Layout'
|
import * as Layout from '#/components/Layout'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
import * as SettingsList from './components/SettingsList'
|
import * as SettingsList from './components/SettingsList'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<
|
type Props = NativeStackScreenProps<
|
||||||
@@ -26,6 +29,7 @@ type Props = NativeStackScreenProps<
|
|||||||
>
|
>
|
||||||
export function FollowingFeedPreferencesScreen({}: Props) {
|
export function FollowingFeedPreferencesScreen({}: Props) {
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
const ax = useAnalytics()
|
||||||
|
|
||||||
const {data: preferences} = usePreferencesQuery()
|
const {data: preferences} = usePreferencesQuery()
|
||||||
const {mutate: setFeedViewPref, variables} =
|
const {mutate: setFeedViewPref, variables} =
|
||||||
@@ -48,6 +52,8 @@ export function FollowingFeedPreferencesScreen({}: Props) {
|
|||||||
preferences?.feedViewPrefs?.lab_mergeFeedEnabled,
|
preferences?.feedViewPrefs?.lab_mergeFeedEnabled,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const [resumeEnabled, setResumeEnabled] = useFollowingFeedResumeEnabled()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Layout.Screen testID="followingFeedPreferencesScreen">
|
<Layout.Screen testID="followingFeedPreferencesScreen">
|
||||||
<Layout.Header.Outer>
|
<Layout.Header.Outer>
|
||||||
@@ -120,6 +126,22 @@ export function FollowingFeedPreferencesScreen({}: Props) {
|
|||||||
<Toggle.Platform />
|
<Toggle.Platform />
|
||||||
</SettingsList.Item>
|
</SettingsList.Item>
|
||||||
</Toggle.Item>
|
</Toggle.Item>
|
||||||
|
{ax.features.enabled(ax.features.FollowingFeedResumeEnable) && (
|
||||||
|
<Toggle.Item
|
||||||
|
type="checkbox"
|
||||||
|
name="remember-place"
|
||||||
|
label={_(msg`Remember your place in the feed`)}
|
||||||
|
value={resumeEnabled}
|
||||||
|
onChange={setResumeEnabled}>
|
||||||
|
<SettingsList.Item>
|
||||||
|
<SettingsList.ItemIcon icon={ClockIcon} />
|
||||||
|
<SettingsList.ItemText>
|
||||||
|
<Trans>Remember your place in the feed</Trans>
|
||||||
|
</SettingsList.ItemText>
|
||||||
|
<Toggle.Platform />
|
||||||
|
</SettingsList.Item>
|
||||||
|
</Toggle.Item>
|
||||||
|
)}
|
||||||
<SettingsList.Divider />
|
<SettingsList.Divider />
|
||||||
<SettingsList.Group>
|
<SettingsList.Group>
|
||||||
<SettingsList.ItemIcon icon={BeakerIcon} />
|
<SettingsList.ItemIcon icon={BeakerIcon} />
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import {useSession} from '#/state/session'
|
||||||
|
import {useAnalytics} from '#/analytics'
|
||||||
|
import {account, useStorage} from '#/storage'
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Positions older than this are discarded rather than restored. Paginating
|
||||||
|
* that far back is slow, and the user has likely moved on.
|
||||||
|
*/
|
||||||
|
const MAX_POSITION_AGE = 24 * 60 * 60 * 1000
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the Following feed should restore the last read position on cold
|
||||||
|
* start. The GrowthBook gate is a hard requirement, so turning the flag off
|
||||||
|
* disables the feature (and hides its settings toggle) for everyone. Within
|
||||||
|
* the gate, the feature is on by default and the user can opt out from the
|
||||||
|
* Following feed preferences screen.
|
||||||
|
*/
|
||||||
|
export function useFollowingFeedResumeEnabled() {
|
||||||
|
const ax = useAnalytics()
|
||||||
|
const {currentAccount} = useSession()
|
||||||
|
const [enabled, setEnabled] = useStorage(account, [
|
||||||
|
currentAccount?.did ?? '',
|
||||||
|
'followingFeedResumeEnabled',
|
||||||
|
])
|
||||||
|
const gateEnabled = ax.features.enabled(ax.features.FollowingFeedResumeEnable)
|
||||||
|
const setEnabledAndTrack = (value: boolean) => {
|
||||||
|
ax.metric('feed:resume:toggle', {enabled: value})
|
||||||
|
setEnabled(value)
|
||||||
|
}
|
||||||
|
return [gateEnabled && (enabled ?? true), setEnabledAndTrack] as const
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Save the most recently viewed post in the Following feed as the anchor to
|
||||||
|
* restore to on next cold start.
|
||||||
|
*/
|
||||||
|
export function saveFollowingFeedPosition(did: string, anchorUri: string) {
|
||||||
|
account.set([did, 'followingFeedPosition'], {
|
||||||
|
anchorUri,
|
||||||
|
savedAt: Date.now(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the saved anchor post URI, or undefined if nothing was saved or the
|
||||||
|
* saved position has expired.
|
||||||
|
*/
|
||||||
|
export function getFollowingFeedPosition(did: string): string | undefined {
|
||||||
|
const position = account.get([did, 'followingFeedPosition'])
|
||||||
|
if (!position || Date.now() - position.savedAt > MAX_POSITION_AGE) {
|
||||||
|
return undefined
|
||||||
|
}
|
||||||
|
return position.anchorUri
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearFollowingFeedPosition(did: string) {
|
||||||
|
account.remove([did, 'followingFeedPosition'])
|
||||||
|
}
|
||||||
@@ -92,6 +92,22 @@ export type Account = {
|
|||||||
|
|
||||||
lastSelectedHomeFeed?: string
|
lastSelectedHomeFeed?: string
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the Following feed restores the last read position on cold start.
|
||||||
|
* Toggled in the Following feed preferences screen. On by default, but the
|
||||||
|
* feature also requires the FollowingFeedResumeEnable gate.
|
||||||
|
*/
|
||||||
|
followingFeedResumeEnabled?: boolean
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The most recently viewed post in the Following feed, used to restore the
|
||||||
|
* feed position on cold start when `followingFeedResumeEnabled` is set.
|
||||||
|
*/
|
||||||
|
followingFeedPosition?: {
|
||||||
|
anchorUri: string
|
||||||
|
savedAt: number
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Recently selected GIFs in the GIF picker. Most recent first, capped at 20.
|
* Recently selected GIFs in the GIF picker. Most recent first, capped at 20.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {getRootNavigation, getTabState, TabState} from '#/lib/routes/helpers'
|
|||||||
import {type AllNavigatorParams} from '#/lib/routes/types'
|
import {type AllNavigatorParams} from '#/lib/routes/types'
|
||||||
import {listenSoftReset} from '#/state/events'
|
import {listenSoftReset} from '#/state/events'
|
||||||
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
import {FeedFeedbackProvider, useFeedFeedback} from '#/state/feed-feedback'
|
||||||
|
import {clearFollowingFeedPosition} from '#/state/feed-position'
|
||||||
import {useSetHomeBadge} from '#/state/home-badge'
|
import {useSetHomeBadge} from '#/state/home-badge'
|
||||||
import {type FeedSourceInfo} from '#/state/queries/feed'
|
import {type FeedSourceInfo} from '#/state/queries/feed'
|
||||||
import {
|
import {
|
||||||
@@ -63,7 +64,7 @@ export function FeedPage({
|
|||||||
feedInfo: FeedSourceInfo
|
feedInfo: FeedSourceInfo
|
||||||
}) {
|
}) {
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const {hasSession} = useSession()
|
const {hasSession, currentAccount} = useSession()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
const navigation = useNavigation<NavigationProp<AllNavigatorParams>>()
|
const navigation = useNavigation<NavigationProp<AllNavigatorParams>>()
|
||||||
const queryClient = useQueryClient()
|
const queryClient = useQueryClient()
|
||||||
@@ -104,13 +105,24 @@ export function FeedPage({
|
|||||||
scrollToTop()
|
scrollToTop()
|
||||||
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
|
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
|
||||||
setHasNew(false)
|
setHasNew(false)
|
||||||
|
if (feed === 'following' && currentAccount) {
|
||||||
|
clearFollowingFeedPosition(currentAccount.did)
|
||||||
|
}
|
||||||
ax.metric('feed:refresh', {
|
ax.metric('feed:refresh', {
|
||||||
feedType: feed.split('|')[0],
|
feedType: feed.split('|')[0],
|
||||||
feedUrl: feed,
|
feedUrl: feed,
|
||||||
reason: 'soft-reset',
|
reason: 'soft-reset',
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}, [ax, navigation, isPageFocused, scrollToTop, queryClient, feed])
|
}, [
|
||||||
|
ax,
|
||||||
|
navigation,
|
||||||
|
isPageFocused,
|
||||||
|
scrollToTop,
|
||||||
|
queryClient,
|
||||||
|
feed,
|
||||||
|
currentAccount,
|
||||||
|
])
|
||||||
|
|
||||||
// fires when page within screen is activated/deactivated
|
// fires when page within screen is activated/deactivated
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -128,12 +140,15 @@ export function FeedPage({
|
|||||||
scrollToTop()
|
scrollToTop()
|
||||||
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
|
truncateAndInvalidate(queryClient, FEED_RQKEY(feed))
|
||||||
setHasNew(false)
|
setHasNew(false)
|
||||||
|
if (feed === 'following' && currentAccount) {
|
||||||
|
clearFollowingFeedPosition(currentAccount.did)
|
||||||
|
}
|
||||||
ax.metric('feed:refresh', {
|
ax.metric('feed:refresh', {
|
||||||
feedType: feed.split('|')[0],
|
feedType: feed.split('|')[0],
|
||||||
feedUrl: feed,
|
feedUrl: feed,
|
||||||
reason: 'load-latest',
|
reason: 'load-latest',
|
||||||
})
|
})
|
||||||
}, [ax, scrollToTop, feed, queryClient])
|
}, [ax, scrollToTop, feed, queryClient, currentAccount])
|
||||||
|
|
||||||
const shouldPrefetch = IS_NATIVE && isPageAdjacent
|
const shouldPrefetch = IS_NATIVE && isPageAdjacent
|
||||||
const isDiscoverFeed = feedInfo.uri === DISCOVER_FEED_URI
|
const isDiscoverFeed = feedInfo.uri === DISCOVER_FEED_URI
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ import {logger} from '#/logger'
|
|||||||
import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow'
|
import {usePostAuthorShadowFilter} from '#/state/cache/profile-shadow'
|
||||||
import {listenPostCreated} from '#/state/events'
|
import {listenPostCreated} from '#/state/events'
|
||||||
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
import {useFeedFeedbackContext} from '#/state/feed-feedback'
|
||||||
|
import {
|
||||||
|
clearFollowingFeedPosition,
|
||||||
|
getFollowingFeedPosition,
|
||||||
|
saveFollowingFeedPosition,
|
||||||
|
useFollowingFeedResumeEnabled,
|
||||||
|
} from '#/state/feed-position'
|
||||||
import {useTrendingSettings} from '#/state/preferences/trending'
|
import {useTrendingSettings} from '#/state/preferences/trending'
|
||||||
import {STALE} from '#/state/queries'
|
import {STALE} from '#/state/queries'
|
||||||
import {
|
import {
|
||||||
@@ -212,6 +218,12 @@ export type PostFeedRef = {
|
|||||||
// const REFRESH_AFTER = STALE.HOURS.ONE
|
// const REFRESH_AFTER = STALE.HOURS.ONE
|
||||||
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
|
const CHECK_LATEST_AFTER = STALE.SECONDS.THIRTY
|
||||||
|
|
||||||
|
/*
|
||||||
|
* When restoring the last read position in the Following feed, fetch at most
|
||||||
|
* this many pages while looking for the anchor post before giving up.
|
||||||
|
*/
|
||||||
|
const RESTORE_MAX_PAGES = 6
|
||||||
|
|
||||||
let PostFeed = ({
|
let PostFeed = ({
|
||||||
feed,
|
feed,
|
||||||
description,
|
description,
|
||||||
@@ -359,6 +371,22 @@ let PostFeed = ({
|
|||||||
onScrolledDownChange?.(isScrolledDown)
|
onScrolledDownChange?.(isScrolledDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const [resumeEnabled] = useFollowingFeedResumeEnabled()
|
||||||
|
/**
|
||||||
|
* The post to restore the Following feed position to, read once at mount.
|
||||||
|
* Restoration is pending until the anchor is found (or we give up);
|
||||||
|
* position saves are paused while pending so the stored anchor isn't
|
||||||
|
* overwritten before we've scrolled to it.
|
||||||
|
*/
|
||||||
|
// oxlint-disable-next-line react/hook-use-state
|
||||||
|
const [restoreAnchorUri] = useState(() =>
|
||||||
|
feed === 'following' && resumeEnabled && currentAccount
|
||||||
|
? getFollowingFeedPosition(currentAccount.did)
|
||||||
|
: undefined,
|
||||||
|
)
|
||||||
|
const restorePendingRef = useRef(restoreAnchorUri != null)
|
||||||
|
const restoreScrollRetriesRef = useRef(0)
|
||||||
|
|
||||||
const myDid = currentAccount?.did || ''
|
const myDid = currentAccount?.did || ''
|
||||||
const onPostCreated = useCallback(() => {
|
const onPostCreated = useCallback(() => {
|
||||||
// NOTE
|
// NOTE
|
||||||
@@ -749,6 +777,101 @@ let PostFeed = ({
|
|||||||
trendingIndices,
|
trendingIndices,
|
||||||
])
|
])
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Restore the last read position on cold start. The anchor post may be
|
||||||
|
* several pages down, so keep fetching (up to RESTORE_MAX_PAGES) until it
|
||||||
|
* shows up in the feed, then scroll to it.
|
||||||
|
*/
|
||||||
|
useEffect(() => {
|
||||||
|
if (!restorePendingRef.current || !restoreAnchorUri || !currentAccount) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!data) return
|
||||||
|
/*
|
||||||
|
* Bail if the user already scrolled away, or if an explicit refresh
|
||||||
|
* (soft reset, pull-to-refresh, "Load new posts") cleared the saved
|
||||||
|
* position while we were looking for the anchor.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
isScrolledDownRef.current ||
|
||||||
|
getFollowingFeedPosition(currentAccount.did) !== restoreAnchorUri
|
||||||
|
) {
|
||||||
|
restorePendingRef.current = false
|
||||||
|
ax.metric('feed:positionRestored', {
|
||||||
|
outcome: 'aborted',
|
||||||
|
pagesFetched: data.pages.length,
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const index = feedItems.findIndex(
|
||||||
|
row =>
|
||||||
|
row.type === 'sliceItem' &&
|
||||||
|
row.slice.items[row.indexInSlice].uri === restoreAnchorUri,
|
||||||
|
)
|
||||||
|
if (index >= 0) {
|
||||||
|
restorePendingRef.current = false
|
||||||
|
scrollElRef?.current?.scrollToIndex({
|
||||||
|
index,
|
||||||
|
animated: false,
|
||||||
|
viewOffset: headerOffset,
|
||||||
|
})
|
||||||
|
ax.metric('feed:positionRestored', {
|
||||||
|
outcome: 'restored',
|
||||||
|
depth: index,
|
||||||
|
pagesFetched: data.pages.length,
|
||||||
|
})
|
||||||
|
} else if (
|
||||||
|
isError ||
|
||||||
|
!hasNextPage ||
|
||||||
|
data.pages.length >= RESTORE_MAX_PAGES
|
||||||
|
) {
|
||||||
|
// the anchor post is gone or too far down - leave the user at the top
|
||||||
|
restorePendingRef.current = false
|
||||||
|
ax.metric('feed:positionRestored', {
|
||||||
|
outcome: 'not-found',
|
||||||
|
pagesFetched: data.pages.length,
|
||||||
|
})
|
||||||
|
} else if (!isFetchingNextPage) {
|
||||||
|
void fetchNextPage()
|
||||||
|
}
|
||||||
|
}, [
|
||||||
|
restoreAnchorUri,
|
||||||
|
currentAccount,
|
||||||
|
data,
|
||||||
|
feedItems,
|
||||||
|
isError,
|
||||||
|
hasNextPage,
|
||||||
|
isFetchingNextPage,
|
||||||
|
fetchNextPage,
|
||||||
|
scrollElRef,
|
||||||
|
headerOffset,
|
||||||
|
ax,
|
||||||
|
])
|
||||||
|
|
||||||
|
const onScrollToIndexFailed = useCallback(
|
||||||
|
(info: {index: number; averageItemLength: number}) => {
|
||||||
|
/*
|
||||||
|
* Native FlatList can't scroll to an index it hasn't rendered yet. Jump
|
||||||
|
* to an estimated offset, then retry once the region has rendered.
|
||||||
|
*/
|
||||||
|
scrollElRef?.current?.scrollToOffset({
|
||||||
|
offset: info.averageItemLength * info.index,
|
||||||
|
animated: false,
|
||||||
|
})
|
||||||
|
if (restoreScrollRetriesRef.current < 3) {
|
||||||
|
restoreScrollRetriesRef.current += 1
|
||||||
|
setTimeout(() => {
|
||||||
|
scrollElRef?.current?.scrollToIndex({
|
||||||
|
index: info.index,
|
||||||
|
animated: false,
|
||||||
|
viewOffset: headerOffset,
|
||||||
|
})
|
||||||
|
}, 100)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[scrollElRef, headerOffset],
|
||||||
|
)
|
||||||
|
|
||||||
// events
|
// events
|
||||||
// =
|
// =
|
||||||
//
|
//
|
||||||
@@ -756,6 +879,9 @@ let PostFeed = ({
|
|||||||
const refreshFeed = async () => {
|
const refreshFeed = async () => {
|
||||||
if (!enabled) return
|
if (!enabled) return
|
||||||
|
|
||||||
|
if (feed === 'following' && currentAccount) {
|
||||||
|
clearFollowingFeedPosition(currentAccount.did)
|
||||||
|
}
|
||||||
ax.metric('feed:refresh', {
|
ax.metric('feed:refresh', {
|
||||||
feedType: feedType,
|
feedType: feedType,
|
||||||
feedUrl: feed,
|
feedUrl: feed,
|
||||||
@@ -1028,6 +1154,24 @@ let PostFeed = ({
|
|||||||
(item: FeedRow) => {
|
(item: FeedRow) => {
|
||||||
feedFeedback.onItemSeen(item)
|
feedFeedback.onItemSeen(item)
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Track the most recently viewed post as the anchor to restore to on
|
||||||
|
* next cold start. Paused while a restore is pending so the target
|
||||||
|
* isn't overwritten by posts seen at the top of the feed.
|
||||||
|
*/
|
||||||
|
if (
|
||||||
|
item.type === 'sliceItem' &&
|
||||||
|
feed === 'following' &&
|
||||||
|
resumeEnabled &&
|
||||||
|
currentAccount &&
|
||||||
|
!restorePendingRef.current
|
||||||
|
) {
|
||||||
|
saveFollowingFeedPosition(
|
||||||
|
currentAccount.did,
|
||||||
|
item.slice.items[item.indexInSlice].uri,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// Events that should fire exactly once for every new post, regardless of
|
// Events that should fire exactly once for every new post, regardless of
|
||||||
// its position within a slice or video grid row.
|
// its position within a slice or video grid row.
|
||||||
const onPostSeen = (post: AppBskyFeedDefs.PostView) => {
|
const onPostSeen = (post: AppBskyFeedDefs.PostView) => {
|
||||||
@@ -1131,7 +1275,15 @@ let PostFeed = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[feedFeedback, feed, liveNowConfig, getPostPosition, ax],
|
[
|
||||||
|
feedFeedback,
|
||||||
|
feed,
|
||||||
|
liveNowConfig,
|
||||||
|
getPostPosition,
|
||||||
|
ax,
|
||||||
|
resumeEnabled,
|
||||||
|
currentAccount,
|
||||||
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -1154,6 +1306,7 @@ let PostFeed = ({
|
|||||||
onScrolledDownChange={handleScrolledDownChange}
|
onScrolledDownChange={handleScrolledDownChange}
|
||||||
onEndReached={() => void onEndReached()}
|
onEndReached={() => void onEndReached()}
|
||||||
onEndReachedThreshold={2} // number of posts left to trigger load more
|
onEndReachedThreshold={2} // number of posts left to trigger load more
|
||||||
|
onScrollToIndexFailed={IS_NATIVE ? onScrollToIndexFailed : undefined}
|
||||||
removeClippedSubviews={true}
|
removeClippedSubviews={true}
|
||||||
extraData={extraData}
|
extraData={extraData}
|
||||||
desktopFixedHeight={
|
desktopFixedHeight={
|
||||||
|
|||||||
Reference in New Issue
Block a user