import React from 'react' import {ActivityIndicator, StyleSheet, View, RefreshControl} from 'react-native' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {FontAwesomeIconStyle} from '@fortawesome/react-native-fontawesome' 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 {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {ComposeIcon2, CogIcon} from 'lib/icons' import {s} from 'lib/styles' import {SearchInput} from 'view/com/util/forms/SearchInput' import {UserAvatar} from 'view/com/util/UserAvatar' import { LoadingPlaceholder, FeedFeedLoadingPlaceholder, } from 'view/com/util/LoadingPlaceholder' import {ErrorMessage} from 'view/com/util/error/ErrorMessage' import debounce from 'lodash.debounce' import {Text} from 'view/com/util/text/Text' import {FlatList} from 'view/com/util/Views' import {useFocusEffect} from '@react-navigation/native' import {FeedSourceCard} 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' import {useComposerControls} from '#/state/shell/composer' import {useSession} from '#/state/session' type Props = NativeStackScreenProps 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 } export function FeedsScreen(_props: Props) { const pal = usePalette('default') const {openComposer} = useComposerControls() 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, hasNextPage: hasNextPopularFeedsPage, } = useGetPopularFeedsQuery() const {_} = useLingui() const setMinimalShellMode = useSetMinimalShellMode() const { data: searchResults, mutate: search, reset: resetSearch, isPending: isSearchPending, error: searchError, } = useSearchPopularFeedsMutation() const {hasSession} = useSession() /** * 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(() => { openComposer({}) }, [openComposer]) 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]) const onEndReached = React.useCallback(() => { if ( isPopularFeedsFetching || isUserSearching || !hasNextPopularFeedsPage || popularFeedsError ) return fetchNextPopularFeedsPage() }, [ isPopularFeedsFetching, isUserSearching, popularFeedsError, hasNextPopularFeedsPage, fetchNextPopularFeedsPage, ]) useFocusEffect( React.useCallback(() => { setMinimalShellMode(false) }, [setMinimalShellMode]), ) const items = React.useMemo(() => { let slices: FlatlistSlice[] = [] if (hasSession) { 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 { const {saved, pinned} = preferences.feeds slices = slices.concat( pinned.map(uri => ({ key: `savedFeed:${uri}`, type: 'savedFeed', feedUri: uri, })), ) 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, })), ) } } } 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 }, [ hasSession, 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 ( My Feeds ) } 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 {!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 }, [ _, hasSession, isMobile, pal, query, onChangeQuery, onPressCancelSearch, onSubmitQuery, ], ) return ( {isMobile && ( )} {preferences ? : } item.key} contentContainerStyle={styles.contentContainer} renderItem={renderItem} refreshControl={ } initialNumToRender={10} onEndReached={onEndReached} // @ts-ignore our .web version only -prf desktopFixedHeight /> {hasSession && ( } 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 ( {error ? ( ) : ( )} {info.displayName} {error ? ( Feed offline ) : null} {isMobile && ( )} ) } function SavedFeedLoadingPlaceholder() { const pal = usePalette('default') const {isMobile} = useWebMediaQueries() return ( ) } const styles = StyleSheet.create({ container: { flex: 1, }, list: { height: '100%', }, contentContainer: { paddingBottom: 100, }, header: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', gap: 16, paddingHorizontal: 16, paddingVertical: 12, }, savedFeed: { flexDirection: 'row', alignItems: 'center', paddingHorizontal: 16, paddingVertical: 14, gap: 12, borderBottomWidth: 1, }, savedFeedMobile: { paddingVertical: 10, }, offlineSlug: { borderWidth: 1, borderRadius: 4, paddingHorizontal: 4, paddingVertical: 2, }, })