Refactor feeds to use react-query (#1862)

* Update to react-query v5

* Introduce post-feed react query

* Add feed refresh behaviors

* Only fetch feeds of visible pages

* Implement polling for latest on feeds

* Add moderation filtering to slices

* Handle block errors

* Update feed error messages

* Remove old models

* Replace simple-feed option with disable-tuner option

* Add missing useMemo

* Implement the mergefeed and fixes to polling

* Correctly handle failed load more state

* Improve error and empty state behaviors

* Clearer naming
This commit is contained in:
Paul Frazee 2023-11-10 15:34:25 -08:00 committed by GitHub
parent 51f04b9620
commit c8c308e31e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 904 additions and 1081 deletions

View file

@ -1,10 +1,9 @@
import React from 'react'
import {useWindowDimensions} from 'react-native'
import {useFocusEffect} from '@react-navigation/native'
import {observer} from 'mobx-react-lite'
import isEqual from 'lodash.isequal'
import {NativeStackScreenProps, HomeTabNavigatorParams} from 'lib/routes/types'
import {PostsFeedModel} from 'state/models/feeds/posts'
import {FeedDescriptor, FeedParams} from '#/state/queries/post-feed'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {FollowingEmptyState} from 'view/com/posts/FollowingEmptyState'
import {FollowingEndOfFeed} from 'view/com/posts/FollowingEndOfFeed'
@ -12,12 +11,9 @@ import {CustomFeedEmptyState} from 'view/com/posts/CustomFeedEmptyState'
import {FeedsTabBar} from '../com/pager/FeedsTabBar'
import {Pager, PagerRef, RenderTabBarFnProps} from 'view/com/pager/Pager'
import {useStores} from 'state/index'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {FeedPage} from 'view/com/feeds/FeedPage'
import {useSetMinimalShellMode, useSetDrawerSwipeDisabled} from '#/state/shell'
export const POLL_FREQ = 30e3 // 30sec
type Props = NativeStackScreenProps<HomeTabNavigatorParams, 'Home'>
export const HomeScreen = withAuthRequired(
observer(function HomeScreenImpl({}: Props) {
@ -26,7 +22,7 @@ export const HomeScreen = withAuthRequired(
const setDrawerSwipeDisabled = useSetDrawerSwipeDisabled()
const pagerRef = React.useRef<PagerRef>(null)
const [selectedPage, setSelectedPage] = React.useState(0)
const [customFeeds, setCustomFeeds] = React.useState<PostsFeedModel[]>([])
const [customFeeds, setCustomFeeds] = React.useState<FeedDescriptor[]>([])
const [requestedCustomFeeds, setRequestedCustomFeeds] = React.useState<
string[]
>([])
@ -39,14 +35,12 @@ export const HomeScreen = withAuthRequired(
return
}
const feeds = []
const feeds: FeedDescriptor[] = []
for (const uri of pinned) {
if (uri.includes('app.bsky.feed.generator')) {
const model = new PostsFeedModel(store, 'custom', {feed: uri})
feeds.push(model)
feeds.push(`feedgen|${uri}`)
} else if (uri.includes('app.bsky.graph.list')) {
const model = new PostsFeedModel(store, 'list', {list: uri})
feeds.push(model)
feeds.push(`list|${uri}`)
}
}
pagerRef.current?.setPage(0)
@ -62,6 +56,19 @@ export const HomeScreen = withAuthRequired(
setRequestedCustomFeeds,
])
const homeFeedParams = React.useMemo<FeedParams>(() => {
if (!store.preferences.homeFeed.lab_mergeFeedEnabled) {
return {}
}
return {
mergeFeedEnabled: true,
mergeFeedSources: store.preferences.savedFeeds,
}
}, [
store.preferences.homeFeed.lab_mergeFeedEnabled,
store.preferences.savedFeeds,
])
useFocusEffect(
React.useCallback(() => {
setMinimalShellMode(false)
@ -129,14 +136,15 @@ export const HomeScreen = withAuthRequired(
key="1"
testID="followingFeedPage"
isPageFocused={selectedPage === 0}
feed={store.me.mainFeed}
feed="home"
feedParams={homeFeedParams}
renderEmptyState={renderFollowingEmptyState}
renderEndOfFeed={FollowingEndOfFeed}
/>
{customFeeds.map((f, index) => {
return (
<FeedPage
key={f.reactKey}
key={f}
testID="customFeedPage"
isPageFocused={selectedPage === 1 + index}
feed={f}
@ -148,17 +156,3 @@ export const HomeScreen = withAuthRequired(
)
}),
)
export function useHeaderOffset() {
const {isDesktop, isTablet} = useWebMediaQueries()
const {fontScale} = useWindowDimensions()
if (isDesktop) {
return 0
}
if (isTablet) {
return 50
}
// default text takes 44px, plus 34px of pad
// scale the 44px by the font scale
return 34 + 44 * fontScale
}

View file

@ -9,7 +9,6 @@ import {CenteredView} from '../com/util/Views'
import {ScreenHider} from 'view/com/util/moderation/ScreenHider'
import {ProfileUiModel, Sections} from 'state/models/ui/profile'
import {useStores} from 'state/index'
import {PostsFeedSliceModel} from 'state/models/feeds/posts-slice'
import {ProfileHeader} from '../com/profile/ProfileHeader'
import {FeedSlice} from '../com/posts/FeedSlice'
import {ListCard} from 'view/com/lists/ListCard'

View file

@ -8,6 +8,7 @@ import {
} from 'react-native'
import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {useNavigation} from '@react-navigation/native'
import {useQueryClient} from '@tanstack/react-query'
import {usePalette} from 'lib/hooks/usePalette'
import {HeartIcon, HeartIconSolid} from 'lib/icons'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
@ -17,7 +18,7 @@ import {colors, s} from 'lib/styles'
import {observer} from 'mobx-react-lite'
import {useStores} from 'state/index'
import {FeedSourceModel} from 'state/models/content/feed-source'
import {PostsFeedModel} from 'state/models/feeds/posts'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
@ -32,6 +33,7 @@ import {EmptyState} from 'view/com/util/EmptyState'
import * as Toast from 'view/com/util/Toast'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {useCustomFeed} from 'lib/hooks/useCustomFeed'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {shareUrl} from 'lib/sharing'
import {toShareUrl} from 'lib/strings/url-helpers'
@ -154,13 +156,6 @@ export const ProfileFeedScreenInner = observer(
[rkey, feedOwnerDid],
)
const feedInfo = useCustomFeed(uri)
const feed: PostsFeedModel = useMemo(() => {
const model = new PostsFeedModel(store, 'custom', {
feed: uri,
})
model.setup()
return model
}, [store, uri])
const isPinned = store.preferences.isPinnedFeed(uri)
useSetTitle(feedInfo?.displayName)
@ -352,7 +347,7 @@ export const ProfileFeedScreenInner = observer(
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
<FeedSection
ref={feedSectionRef}
feed={feed}
feed={`feedgen|${uri}`}
onScroll={onScroll}
headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
@ -395,7 +390,7 @@ export const ProfileFeedScreenInner = observer(
)
interface FeedSectionProps {
feed: PostsFeedModel
feed: FeedDescriptor
onScroll: OnScrollHandler
headerHeight: number
isScrolledDown: boolean
@ -406,12 +401,14 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
{feed, onScroll, headerHeight, isScrolledDown, scrollElRef},
ref,
) {
const hasNew = feed.hasNewLatest && !feed.isRefreshing
const [hasNew, setHasNew] = React.useState(false)
const queryClient = useQueryClient()
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
feed.refresh()
}, [feed, scrollElRef, headerHeight])
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
React.useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop,
@ -425,7 +422,9 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
<View>
<Feed
feed={feed}
pollInterval={30e3}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScroll={onScroll}
scrollEventThrottle={5}
renderEmptyState={renderPostsEmpty}

View file

@ -12,6 +12,7 @@ import {useNavigation} from '@react-navigation/native'
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
import {observer} from 'mobx-react-lite'
import {RichText as RichTextAPI} from '@atproto/api'
import {useQueryClient} from '@tanstack/react-query'
import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {PagerWithHeader} from 'view/com/pager/PagerWithHeader'
import {ProfileSubpageHeader} from 'view/com/profile/ProfileSubpageHeader'
@ -28,11 +29,12 @@ import {LoadLatestBtn} from 'view/com/util/load-latest/LoadLatestBtn'
import {FAB} from 'view/com/util/fab/FAB'
import {Haptics} from 'lib/haptics'
import {ListModel} from 'state/models/content/list'
import {PostsFeedModel} from 'state/models/feeds/posts'
import {FeedDescriptor} from '#/state/queries/post-feed'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette'
import {useSetTitle} from 'lib/hooks/useSetTitle'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {RQKEY as FEED_RQKEY} from '#/state/queries/post-feed'
import {OnScrollHandler} from 'lib/hooks/useOnMainScroll'
import {NavigationProp} from 'lib/routes/types'
import {toShareUrl} from 'lib/strings/url-helpers'
@ -109,34 +111,25 @@ export const ProfileListScreenInner = observer(
}: Props & {listOwnerDid: string}) {
const store = useStores()
const {_} = useLingui()
const queryClient = useQueryClient()
const setMinimalShellMode = useSetMinimalShellMode()
const {rkey} = route.params
const listUri = `at://${listOwnerDid}/app.bsky.graph.list/${rkey}`
const feedSectionRef = React.useRef<SectionRef>(null)
const aboutSectionRef = React.useRef<SectionRef>(null)
const {openModal} = useModalControls()
const list: ListModel = useMemo(() => {
const model = new ListModel(
store,
`at://${listOwnerDid}/app.bsky.graph.list/${rkey}`,
)
const model = new ListModel(store, listUri)
return model
}, [store, listOwnerDid, rkey])
const feed = useMemo(
() => new PostsFeedModel(store, 'list', {list: list.uri}),
[store, list],
)
}, [store, listUri])
useSetTitle(list.data?.name)
useFocusEffect(
useCallback(() => {
setMinimalShellMode(false)
list.loadMore(true).then(() => {
if (list.isCuratelist) {
feed.setup()
}
})
}, [setMinimalShellMode, list, feed]),
list.loadMore(true)
}, [setMinimalShellMode, list]),
)
const onPressAddUser = useCallback(() => {
@ -145,11 +138,13 @@ export const ProfileListScreenInner = observer(
list,
onAdd() {
if (list.isCuratelist) {
feed.refresh()
queryClient.invalidateQueries({
queryKey: FEED_RQKEY(`list|${listUri}`),
})
}
},
})
}, [openModal, list, feed])
}, [openModal, list, queryClient, listUri])
const onCurrentPageSelected = React.useCallback(
(index: number) => {
@ -178,10 +173,10 @@ export const ProfileListScreenInner = observer(
{({onScroll, headerHeight, isScrolledDown, scrollElRef}) => (
<FeedSection
ref={feedSectionRef}
feed={`list|${listUri}`}
scrollElRef={
scrollElRef as React.MutableRefObject<FlatList<any> | null>
}
feed={feed}
onScroll={onScroll}
headerHeight={headerHeight}
isScrolledDown={isScrolledDown}
@ -562,7 +557,7 @@ const Header = observer(function HeaderImpl({
})
interface FeedSectionProps {
feed: PostsFeedModel
feed: FeedDescriptor
onScroll: OnScrollHandler
headerHeight: number
isScrolledDown: boolean
@ -573,12 +568,14 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
{feed, scrollElRef, onScroll, headerHeight, isScrolledDown},
ref,
) {
const hasNew = feed.hasNewLatest && !feed.isRefreshing
const queryClient = useQueryClient()
const [hasNew, setHasNew] = React.useState(false)
const onScrollToTop = useCallback(() => {
scrollElRef.current?.scrollToOffset({offset: -headerHeight})
feed.refresh()
}, [feed, scrollElRef, headerHeight])
queryClient.invalidateQueries({queryKey: FEED_RQKEY(feed)})
setHasNew(false)
}, [scrollElRef, headerHeight, queryClient, feed, setHasNew])
React.useImperativeHandle(ref, () => ({
scrollToTop: onScrollToTop,
}))
@ -592,7 +589,9 @@ const FeedSection = React.forwardRef<SectionRef, FeedSectionProps>(
<Feed
testID="listFeed"
feed={feed}
pollInterval={30e3}
scrollElRef={scrollElRef}
onHasNew={setHasNew}
onScroll={onScroll}
scrollEventThrottle={1}
renderEmptyState={renderPostsEmpty}