From c584a3378d66459c04eee7d98560920e09c5f09f Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Sun, 12 Nov 2023 18:26:02 -0600 Subject: [PATCH] Refactor My Feeds (#1877) * Refactor My Feeds screen * Remove unused feed UI models * Add back PTR --- src/state/models/me.ts | 6 - src/state/models/ui/my-feeds.ts | 182 -------- src/state/models/ui/saved-feeds.ts | 122 ----- src/state/queries/feed.ts | 70 ++- src/view/screens/Feeds.tsx | 694 +++++++++++++++++++---------- 5 files changed, 532 insertions(+), 542 deletions(-) delete mode 100644 src/state/models/ui/my-feeds.ts delete mode 100644 src/state/models/ui/saved-feeds.ts diff --git a/src/state/models/me.ts b/src/state/models/me.ts index 4bbb5a04..c17fcf18 100644 --- a/src/state/models/me.ts +++ b/src/state/models/me.ts @@ -5,7 +5,6 @@ import { } from '@atproto/api' import {RootStoreModel} from './root-store' import {NotificationsFeedModel} from './feeds/notifications' -import {MyFeedsUIModel} from './ui/my-feeds' import {MyFollowsCache} from './cache/my-follows' import {isObj, hasProp} from 'lib/type-guards' import {logger} from '#/logger' @@ -22,7 +21,6 @@ export class MeModel { followsCount: number | undefined followersCount: number | undefined notifications: NotificationsFeedModel - myFeeds: MyFeedsUIModel follows: MyFollowsCache invites: ComAtprotoServerDefs.InviteCode[] = [] appPasswords: ComAtprotoServerListAppPasswords.AppPassword[] = [] @@ -40,13 +38,11 @@ export class MeModel { {autoBind: true}, ) this.notifications = new NotificationsFeedModel(this.rootStore) - this.myFeeds = new MyFeedsUIModel(this.rootStore) this.follows = new MyFollowsCache(this.rootStore) } clear() { this.notifications.clear() - this.myFeeds.clear() this.follows.clear() this.rootStore.profiles.cache.clear() this.rootStore.posts.cache.clear() @@ -113,8 +109,6 @@ export class MeModel { error: e, }) }) - this.myFeeds.clear() - /* dont await */ this.myFeeds.saved.refresh() this.rootStore.emitSessionLoaded() await this.fetchInviteCodes() await this.fetchAppPasswords() diff --git a/src/state/models/ui/my-feeds.ts b/src/state/models/ui/my-feeds.ts deleted file mode 100644 index ade68633..00000000 --- a/src/state/models/ui/my-feeds.ts +++ /dev/null @@ -1,182 +0,0 @@ -import {makeAutoObservable, reaction} from 'mobx' -import {SavedFeedsModel} from './saved-feeds' -import {FeedsDiscoveryModel} from '../discovery/feeds' -import {FeedSourceModel} from '../content/feed-source' -import {RootStoreModel} from '../root-store' - -export type MyFeedsItem = - | { - _reactKey: string - type: 'spinner' - } - | { - _reactKey: string - type: 'saved-feeds-loading' - numItems: number - } - | { - _reactKey: string - type: 'discover-feeds-loading' - } - | { - _reactKey: string - type: 'error' - error: string - } - | { - _reactKey: string - type: 'saved-feeds-header' - } - | { - _reactKey: string - type: 'saved-feed' - feed: FeedSourceModel - } - | { - _reactKey: string - type: 'saved-feeds-load-more' - } - | { - _reactKey: string - type: 'discover-feeds-header' - } - | { - _reactKey: string - type: 'discover-feeds-no-results' - } - | { - _reactKey: string - type: 'discover-feed' - feed: FeedSourceModel - } - -export class MyFeedsUIModel { - saved: SavedFeedsModel - discovery: FeedsDiscoveryModel - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable(this) - this.saved = new SavedFeedsModel(this.rootStore) - this.discovery = new FeedsDiscoveryModel(this.rootStore) - } - - get isRefreshing() { - return !this.saved.isLoading && this.saved.isRefreshing - } - - get isLoading() { - return this.saved.isLoading || this.discovery.isLoading - } - - async setup() { - if (!this.saved.hasLoaded) { - await this.saved.refresh() - } - if (!this.discovery.hasLoaded) { - await this.discovery.refresh() - } - } - - clear() { - this.saved.clear() - this.discovery.clear() - } - - registerListeners() { - const dispose1 = reaction( - () => this.rootStore.preferences.savedFeeds, - () => this.saved.refresh(), - ) - const dispose2 = reaction( - () => this.rootStore.preferences.pinnedFeeds, - () => this.saved.refresh(), - ) - return () => { - dispose1() - dispose2() - } - } - - async refresh() { - return Promise.all([this.saved.refresh(), this.discovery.refresh()]) - } - - async loadMore() { - return this.discovery.loadMore() - } - - get items() { - let items: MyFeedsItem[] = [] - - items.push({ - _reactKey: '__saved_feeds_header__', - type: 'saved-feeds-header', - }) - if (this.saved.isLoading && !this.saved.hasContent) { - items.push({ - _reactKey: '__saved_feeds_loading__', - type: 'saved-feeds-loading', - numItems: this.rootStore.preferences.savedFeeds.length || 3, - }) - } else if (this.saved.hasError) { - items.push({ - _reactKey: '__saved_feeds_error__', - type: 'error', - error: this.saved.error, - }) - } else { - const savedSorted = this.saved.all - .slice() - .sort((a, b) => a.displayName.localeCompare(b.displayName)) - items = items.concat( - savedSorted.map(feed => ({ - _reactKey: `saved-${feed.uri}`, - type: 'saved-feed', - feed, - })), - ) - items.push({ - _reactKey: '__saved_feeds_load_more__', - type: 'saved-feeds-load-more', - }) - } - - items.push({ - _reactKey: '__discover_feeds_header__', - type: 'discover-feeds-header', - }) - if (this.discovery.isLoading && !this.discovery.hasContent) { - items.push({ - _reactKey: '__discover_feeds_loading__', - type: 'discover-feeds-loading', - }) - } else if (this.discovery.hasError) { - items.push({ - _reactKey: '__discover_feeds_error__', - type: 'error', - error: this.discovery.error, - }) - } else if (this.discovery.isEmpty) { - items.push({ - _reactKey: '__discover_feeds_no_results__', - type: 'discover-feeds-no-results', - }) - } else { - items = items.concat( - this.discovery.feeds.map(feed => ({ - _reactKey: `discover-${feed.uri}`, - type: 'discover-feed', - feed, - })), - ) - if (this.discovery.isLoading) { - items.push({ - _reactKey: '__discover_feeds_loading_more__', - type: 'spinner', - }) - } - } - - return items - } -} diff --git a/src/state/models/ui/saved-feeds.ts b/src/state/models/ui/saved-feeds.ts deleted file mode 100644 index cf4cf6d7..00000000 --- a/src/state/models/ui/saved-feeds.ts +++ /dev/null @@ -1,122 +0,0 @@ -import {makeAutoObservable, runInAction} from 'mobx' -import {RootStoreModel} from '../root-store' -import {bundleAsync} from 'lib/async/bundle' -import {cleanError} from 'lib/strings/errors' -import {FeedSourceModel} from '../content/feed-source' -import {logger} from '#/logger' - -export class SavedFeedsModel { - // state - isLoading = false - isRefreshing = false - hasLoaded = false - error = '' - - // data - all: FeedSourceModel[] = [] - - constructor(public rootStore: RootStoreModel) { - makeAutoObservable( - this, - { - rootStore: false, - }, - {autoBind: true}, - ) - } - - get hasContent() { - return this.all.length > 0 - } - - get hasError() { - return this.error !== '' - } - - get isEmpty() { - return this.hasLoaded && !this.hasContent - } - - get pinned(): FeedSourceModel[] { - return this.rootStore.preferences.savedFeeds - .filter(feed => this.rootStore.preferences.isPinnedFeed(feed)) - .map(uri => this.all.find(f => f.uri === uri)) - .filter(Boolean) as FeedSourceModel[] - } - - get unpinned(): FeedSourceModel[] { - return this.rootStore.preferences.savedFeeds - .filter(feed => !this.rootStore.preferences.isPinnedFeed(feed)) - .map(uri => this.all.find(f => f.uri === uri)) - .filter(Boolean) as FeedSourceModel[] - } - - get pinnedFeedNames() { - return this.pinned.map(f => f.displayName) - } - - // public api - // = - - clear() { - this.all = [] - } - - /** - * Refresh the preferences then reload all feed infos - */ - refresh = bundleAsync(async () => { - this._xLoading(true) - try { - const uris = dedup( - this.rootStore.preferences.pinnedFeeds.concat( - this.rootStore.preferences.savedFeeds, - ), - ) - const feeds = uris.map(uri => new FeedSourceModel(this.rootStore, uri)) - await Promise.all(feeds.map(f => f.setup())) - runInAction(() => { - this.all = feeds - this._updatePinSortOrder() - }) - this._xIdle() - } catch (e: any) { - this._xIdle(e) - } - }) - - // state transitions - // = - - _xLoading(isRefreshing = false) { - this.isLoading = true - this.isRefreshing = isRefreshing - this.error = '' - } - - _xIdle(err?: any) { - this.isLoading = false - this.isRefreshing = false - this.hasLoaded = true - this.error = cleanError(err) - if (err) { - logger.error('Failed to fetch user feeds', {err}) - } - } - - // helpers - // = - - _updatePinSortOrder(order?: string[]) { - order ??= this.rootStore.preferences.pinnedFeeds.concat( - this.rootStore.preferences.savedFeeds, - ) - this.all.sort((a, b) => { - return order!.indexOf(a.uri) - order!.indexOf(b.uri) - }) - } -} - -function dedup(strings: string[]): string[] { - return Array.from(new Set(strings)) -} diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 0ba32331..5754d2c7 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -1,5 +1,17 @@ -import {useQuery} from '@tanstack/react-query' -import {AtUri, RichText, AppBskyFeedDefs, AppBskyGraphDefs} from '@atproto/api' +import { + useQuery, + useInfiniteQuery, + InfiniteData, + QueryKey, + useMutation, +} from '@tanstack/react-query' +import { + AtUri, + RichText, + AppBskyFeedDefs, + AppBskyGraphDefs, + AppBskyUnspeccedGetPopularFeedGenerators, +} from '@atproto/api' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' @@ -10,6 +22,7 @@ type FeedSourceInfo = type: 'feed' uri: string cid: string + href: string avatar: string | undefined displayName: string description: RichText @@ -22,6 +35,7 @@ type FeedSourceInfo = type: 'list' uri: string cid: string + href: string avatar: string | undefined displayName: string description: RichText @@ -42,10 +56,16 @@ const feedSourceNSIDs = { function hydrateFeedGenerator( view: AppBskyFeedDefs.GeneratorView, ): FeedSourceInfo { + const urip = new AtUri(view.uri) + const collection = + urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'lists' + const href = `/profile/${urip.hostname}/${collection}/${urip.rkey}` + return { type: 'feed', uri: view.uri, cid: view.cid, + href, avatar: view.avatar, displayName: view.displayName ? sanitizeDisplayName(view.displayName) @@ -62,10 +82,16 @@ function hydrateFeedGenerator( } function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo { + const urip = new AtUri(view.uri) + const collection = + urip.collection === 'app.bsky.feed.generator' ? 'feed' : 'lists' + const href = `/profile/${urip.hostname}/${collection}/${urip.rkey}` + return { type: 'list', uri: view.uri, cid: view.cid, + href, avatar: view.avatar, description: new RichText({ text: view.description || '', @@ -104,3 +130,43 @@ export function useFeedSourceInfoQuery({uri}: {uri: string}) { }, }) } + +export const useGetPopularFeedsQueryKey = ['getPopularFeeds'] + +export function useGetPopularFeedsQuery() { + const {agent} = useSession() + + return useInfiniteQuery< + AppBskyUnspeccedGetPopularFeedGenerators.OutputSchema, + Error, + InfiniteData, + QueryKey, + string | undefined + >({ + queryKey: useGetPopularFeedsQueryKey, + queryFn: async ({pageParam}) => { + const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ + limit: 10, + cursor: pageParam, + }) + return res.data + }, + initialPageParam: undefined, + getNextPageParam: lastPage => lastPage.cursor, + }) +} + +export function useSearchPopularFeedsMutation() { + const {agent} = useSession() + + return useMutation({ + mutationFn: async (query: string) => { + const res = await agent.app.bsky.unspecced.getPopularFeedGenerators({ + limit: 10, + query: query, + }) + + return res.data.feeds + }, + }) +} diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index c2ec9208..c78f44cd 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {ActivityIndicator, StyleSheet, RefreshControl, View} from 'react-native' +import {ActivityIndicator, StyleSheet, View, RefreshControl} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' import {withAuthRequired} from 'view/com/auth/withAuthRequired' @@ -7,7 +7,6 @@ import {ViewHeader} from 'view/com/util/ViewHeader' import {FAB} from 'view/com/util/fab/FAB' import {Link} from 'view/com/util/Link' import {NativeStackScreenProps, FeedsTabNavigatorParams} from 'lib/routes/types' -import {observer} from 'mobx-react-lite' import {usePalette} from 'lib/hooks/usePalette' import {useStores} from 'state/index' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' @@ -22,266 +21,501 @@ import { import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import debounce from 'lodash.debounce' import {Text} from 'view/com/util/text/Text' -import {MyFeedsItem} from 'state/models/ui/my-feeds' -import {FeedSourceModel} from 'state/models/content/feed-source' import {FlatList} from 'view/com/util/Views' import {useFocusEffect} from '@react-navigation/native' -import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard' +import {NewFeedSourceCard} from 'view/com/feeds/FeedSourceCard' import {Trans, msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useSetMinimalShellMode} from '#/state/shell' +import {usePreferencesQuery} from '#/state/queries/preferences' +import { + useFeedSourceInfoQuery, + useGetPopularFeedsQuery, + useSearchPopularFeedsMutation, +} from '#/state/queries/feed' +import {cleanError} from 'lib/strings/errors' type Props = NativeStackScreenProps -export const FeedsScreen = withAuthRequired( - observer(function FeedsScreenImpl({}: Props) { - const pal = usePalette('default') - const store = useStores() - const {_} = useLingui() - const setMinimalShellMode = useSetMinimalShellMode() - const {isMobile, isTabletOrDesktop} = useWebMediaQueries() - const myFeeds = store.me.myFeeds - const [query, setQuery] = React.useState('') - const debouncedSearchFeeds = React.useMemo( - () => debounce(q => myFeeds.discovery.search(q), 500), // debounce for 500ms - [myFeeds], - ) - useFocusEffect( - React.useCallback(() => { - setMinimalShellMode(false) - myFeeds.setup() +type FlatlistSlice = + | { + type: 'error' + key: string + error: string + } + | { + type: 'savedFeedsHeader' + key: string + } + | { + type: 'savedFeedsLoading' + key: string + // pendingItems: number, + } + | { + type: 'savedFeedNoResults' + key: string + } + | { + type: 'savedFeed' + key: string + feedUri: string + } + | { + type: 'savedFeedsLoadMore' + key: string + } + | { + type: 'popularFeedsHeader' + key: string + } + | { + type: 'popularFeedsLoading' + key: string + } + | { + type: 'popularFeedsNoResults' + key: string + } + | { + type: 'popularFeed' + key: string + feedUri: string + } + | { + type: 'popularFeedsLoadingMore' + key: string + } - const softResetSub = store.onScreenSoftReset(() => myFeeds.refresh()) - return () => { - softResetSub.remove() - } - }, [store, myFeeds, setMinimalShellMode]), - ) - React.useEffect(() => { - // watch for changes to saved/pinned feeds - return myFeeds.registerListeners() - }, [myFeeds]) +export const FeedsScreen = withAuthRequired(function FeedsScreenImpl( + _props: Props, +) { + const store = useStores() + const pal = usePalette('default') + const {isMobile, isTabletOrDesktop} = useWebMediaQueries() + const [query, setQuery] = React.useState('') + const [isPTR, setIsPTR] = React.useState(false) + const { + data: preferences, + isLoading: isPreferencesLoading, + error: preferencesError, + } = usePreferencesQuery() + const { + data: popularFeeds, + isFetching: isPopularFeedsFetching, + error: popularFeedsError, + refetch: refetchPopularFeeds, + fetchNextPage: fetchNextPopularFeedsPage, + isFetchingNextPage: isPopularFeedsFetchingNextPage, + } = useGetPopularFeedsQuery() + const {_} = useLingui() + const setMinimalShellMode = useSetMinimalShellMode() + const { + data: searchResults, + mutate: search, + reset: resetSearch, + isPending: isSearchPending, + error: searchError, + } = useSearchPopularFeedsMutation() - const onPressCompose = React.useCallback(() => { - store.shell.openComposer({}) - }, [store]) - const onChangeQuery = React.useCallback( - (text: string) => { - setQuery(text) - if (text.length > 1) { - debouncedSearchFeeds(text) + /** + * A search query is present. We may not have search results yet. + */ + const isUserSearching = query.length > 1 + const debouncedSearch = React.useMemo( + () => debounce(q => search(q), 500), // debounce for 500ms + [search], + ) + const onPressCompose = React.useCallback(() => { + store.shell.openComposer({}) + }, [store]) + const onChangeQuery = React.useCallback( + (text: string) => { + setQuery(text) + if (text.length > 1) { + debouncedSearch(text) + } else { + refetchPopularFeeds() + resetSearch() + } + }, + [setQuery, refetchPopularFeeds, debouncedSearch, resetSearch], + ) + const onPressCancelSearch = React.useCallback(() => { + setQuery('') + refetchPopularFeeds() + resetSearch() + }, [refetchPopularFeeds, setQuery, resetSearch]) + const onSubmitQuery = React.useCallback(() => { + debouncedSearch(query) + }, [query, debouncedSearch]) + const onPullToRefresh = React.useCallback(async () => { + setIsPTR(true) + await refetchPopularFeeds() + setIsPTR(false) + }, [setIsPTR, refetchPopularFeeds]) + + useFocusEffect( + React.useCallback(() => { + setMinimalShellMode(false) + }, [setMinimalShellMode]), + ) + + const items = React.useMemo(() => { + let slices: FlatlistSlice[] = [] + + slices.push({ + key: 'savedFeedsHeader', + type: 'savedFeedsHeader', + }) + + if (preferencesError) { + slices.push({ + key: 'savedFeedsError', + type: 'error', + error: cleanError(preferencesError.toString()), + }) + } else { + if (isPreferencesLoading || !preferences?.feeds?.saved) { + slices.push({ + key: 'savedFeedsLoading', + type: 'savedFeedsLoading', + // pendingItems: this.rootStore.preferences.savedFeeds.length || 3, + }) + } else { + if (preferences?.feeds?.saved.length === 0) { + slices.push({ + key: 'savedFeedNoResults', + type: 'savedFeedNoResults', + }) } else { - myFeeds.discovery.refresh() - } - }, - [debouncedSearchFeeds, myFeeds.discovery], - ) - const onPressCancelSearch = React.useCallback(() => { - setQuery('') - myFeeds.discovery.refresh() - }, [myFeeds]) - const onSubmitQuery = React.useCallback(() => { - debouncedSearchFeeds(query) - debouncedSearchFeeds.flush() - }, [debouncedSearchFeeds, query]) + const {saved, pinned} = preferences.feeds - const renderHeaderBtn = React.useCallback(() => { - return ( - - - - ) - }, [pal, _]) - - const onRefresh = React.useCallback(() => { - myFeeds.refresh() - }, [myFeeds]) - - const renderItem = React.useCallback( - ({item}: {item: MyFeedsItem}) => { - if (item.type === 'discover-feeds-loading') { - return - } else if (item.type === 'spinner') { - return ( - - - + slices = slices.concat( + pinned.map(uri => ({ + key: `savedFeed:${uri}`, + type: 'savedFeed', + feedUri: uri, + })), ) - } else if (item.type === 'error') { - return - } else if (item.type === 'saved-feeds-header') { - if (!isMobile) { - return ( - - - My Feeds - - - - - + + slices = slices.concat( + saved + .filter(uri => !pinned.includes(uri)) + .map(uri => ({ + key: `savedFeed:${uri}`, + type: 'savedFeed', + feedUri: uri, + })), + ) + } + } + } + + slices.push({ + key: 'popularFeedsHeader', + type: 'popularFeedsHeader', + }) + + if (popularFeedsError || searchError) { + slices.push({ + key: 'popularFeedsError', + type: 'error', + error: cleanError( + popularFeedsError?.toString() ?? searchError?.toString() ?? '', + ), + }) + } else { + if (isUserSearching) { + if (isSearchPending || !searchResults) { + slices.push({ + key: 'popularFeedsLoading', + type: 'popularFeedsLoading', + }) + } else { + if (!searchResults || searchResults?.length === 0) { + slices.push({ + key: 'popularFeedsNoResults', + type: 'popularFeedsNoResults', + }) + } else { + slices = slices.concat( + searchResults.map(feed => ({ + key: `popularFeed:${feed.uri}`, + type: 'popularFeed', + feedUri: feed.uri, + })), ) } - return - } else if (item.type === 'saved-feeds-loading') { - return ( - <> - {Array.from(Array(item.numItems)).map((_i, i) => ( - - ))} - - ) - } else if (item.type === 'saved-feed') { - return - } else if (item.type === 'discover-feeds-header') { - return ( - <> - - - Discover new feeds - - {!isMobile && ( - - )} - - {isMobile && ( - - - - )} - - ) - } else if (item.type === 'discover-feed') { - return ( - - ) - } else if (item.type === 'discover-feeds-no-results') { + } + } else { + if (isPopularFeedsFetching && !popularFeeds?.pages) { + slices.push({ + key: 'popularFeedsLoading', + type: 'popularFeedsLoading', + }) + } else { + if ( + !popularFeeds?.pages || + popularFeeds?.pages[0]?.feeds?.length === 0 + ) { + slices.push({ + key: 'popularFeedsNoResults', + type: 'popularFeedsNoResults', + }) + } else { + for (const page of popularFeeds.pages || []) { + slices = slices.concat( + page.feeds + .filter(feed => !preferences?.feeds?.saved.includes(feed.uri)) + .map(feed => ({ + key: `popularFeed:${feed.uri}`, + type: 'popularFeed', + feedUri: feed.uri, + })), + ) + } + + if (isPopularFeedsFetchingNextPage) { + slices.push({ + key: 'popularFeedsLoadingMore', + type: 'popularFeedsLoadingMore', + }) + } + } + } + } + } + + return slices + }, [ + preferences, + isPreferencesLoading, + preferencesError, + popularFeeds, + isPopularFeedsFetching, + popularFeedsError, + isPopularFeedsFetchingNextPage, + searchResults, + isSearchPending, + searchError, + isUserSearching, + ]) + + const renderHeaderBtn = React.useCallback(() => { + return ( + + + + ) + }, [pal, _]) + + const renderItem = React.useCallback( + ({item}: {item: FlatlistSlice}) => { + if (item.type === 'error') { + return + } else if ( + item.type === 'popularFeedsLoadingMore' || + item.type === 'savedFeedsLoading' + ) { + return ( + + + + ) + } else if (item.type === 'savedFeedsHeader') { + if (!isMobile) { return ( - - No results found for "{query}" + style={[ + pal.view, + styles.header, + pal.border, + { + borderBottomWidth: 1, + }, + ]}> + + My Feeds + + + ) } - return null - }, - [ - isMobile, - pal, - query, - onChangeQuery, - onPressCancelSearch, - onSubmitQuery, - _, - ], - ) + return + } else if (item.type === 'savedFeedNoResults') { + return ( + + + You don't have any saved feeds! + + + ) + } else if (item.type === 'savedFeed') { + return + } else if (item.type === 'popularFeedsHeader') { + return ( + <> + + + Discover new feeds + - return ( - - {isMobile && ( - + )} + + + {isMobile && ( + + + + )} + + ) + } else if (item.type === 'popularFeedsLoading') { + return + } else if (item.type === 'popularFeed') { + return ( + - )} + ) + } else if (item.type === 'popularFeedsNoResults') { + return ( + + + No results found for "{query}" + + + ) + } + return null + }, + [ + _, + isMobile, + pal, + query, + onChangeQuery, + onPressCancelSearch, + onSubmitQuery, + ], + ) - item._reactKey} - contentContainerStyle={styles.contentContainer} - refreshControl={ - - } - renderItem={renderItem} - initialNumToRender={10} - onEndReached={() => myFeeds.loadMore()} - extraData={myFeeds.isLoading} - // @ts-ignore our .web version only -prf - desktopFixedHeight + return ( + + {isMobile && ( + - } - accessibilityRole="button" - accessibilityLabel={_(msg`New post`)} - accessibilityHint="" - /> - - ) - }), -) + )} -function SavedFeed({feed}: {feed: FeedSourceModel}) { + {preferences ? : } + + item.key} + contentContainerStyle={styles.contentContainer} + renderItem={renderItem} + refreshControl={ + + } + initialNumToRender={10} + onEndReached={() => + isUserSearching ? undefined : fetchNextPopularFeedsPage() + } + // @ts-ignore our .web version only -prf + desktopFixedHeight + /> + + } + accessibilityRole="button" + accessibilityLabel={_(msg`New post`)} + accessibilityHint="" + /> + + ) +}) + +function SavedFeed({feedUri}: {feedUri: string}) { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() + const {data: info, error} = useFeedSourceInfoQuery({uri: feedUri}) + + if (!info) + return ( + + ) + return ( - {feed.error ? ( + {error ? ( ) : ( - + )} - {feed.displayName} + {info.displayName} - {feed.error ? ( + {error ? ( Feed offline