diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/Navigation.tsx | 6 | ||||
-rw-r--r-- | src/lib/routes/types.ts | 1 | ||||
-rw-r--r-- | src/routes.ts | 1 | ||||
-rw-r--r-- | src/state/models/discovery/feeds.ts | 97 | ||||
-rw-r--r-- | src/view/com/feeds/SavedFeeds.tsx | 41 | ||||
-rw-r--r-- | src/view/screens/CustomFeed.tsx | 6 | ||||
-rw-r--r-- | src/view/screens/DiscoverFeeds.tsx | 109 |
7 files changed, 246 insertions, 15 deletions
diff --git a/src/Navigation.tsx b/src/Navigation.tsx index ff7a5f5c2..0664ac526 100644 --- a/src/Navigation.tsx +++ b/src/Navigation.tsx @@ -35,6 +35,7 @@ import {SearchScreen} from './view/screens/Search' import {NotificationsScreen} from './view/screens/Notifications' import {ModerationScreen} from './view/screens/Moderation' import {ModerationMuteListsScreen} from './view/screens/ModerationMuteLists' +import {DiscoverFeedsScreen} from 'view/screens/DiscoverFeeds' import {NotFoundScreen} from './view/screens/NotFound' import {SettingsScreen} from './view/screens/Settings' import {ProfileScreen} from './view/screens/Profile' @@ -104,6 +105,11 @@ function commonScreens(Stack: typeof HomeTab, unreadCountLabel?: string) { options={{title: title('Blocked Accounts')}} /> <Stack.Screen + name="DiscoverFeeds" + component={DiscoverFeedsScreen} + options={{title: title('Discover Feeds')}} + /> + <Stack.Screen name="Settings" component={SettingsScreen} options={{title: title('Settings')}} diff --git a/src/lib/routes/types.ts b/src/lib/routes/types.ts index b714c10c0..5dca3cc3f 100644 --- a/src/lib/routes/types.ts +++ b/src/lib/routes/types.ts @@ -9,6 +9,7 @@ export type CommonNavigatorParams = { ModerationMuteLists: undefined ModerationMutedAccounts: undefined ModerationBlockedAccounts: undefined + DiscoverFeeds: undefined Settings: undefined Profile: {name: string; hideBackButton?: boolean} ProfileFollowers: {name: string} diff --git a/src/routes.ts b/src/routes.ts index 9e3e3f7ea..1c3d91187 100644 --- a/src/routes.ts +++ b/src/routes.ts @@ -3,6 +3,7 @@ import {Router} from 'lib/routes/router' export const router = new Router({ Home: '/', Search: '/search', + DiscoverFeeds: '/search/feeds', Notifications: '/notifications', Settings: '/settings', Moderation: '/moderation', diff --git a/src/state/models/discovery/feeds.ts b/src/state/models/discovery/feeds.ts new file mode 100644 index 000000000..26a8d650c --- /dev/null +++ b/src/state/models/discovery/feeds.ts @@ -0,0 +1,97 @@ +import {makeAutoObservable} from 'mobx' +import {AppBskyUnspeccedGetPopularFeedGenerators} from '@atproto/api' +import {RootStoreModel} from '../root-store' +import {bundleAsync} from 'lib/async/bundle' +import {cleanError} from 'lib/strings/errors' +import {CustomFeedModel} from '../feeds/custom-feed' + +export class FeedsDiscoveryModel { + // state + isLoading = false + isRefreshing = false + hasLoaded = false + error = '' + + // data + feeds: CustomFeedModel[] = [] + + constructor(public rootStore: RootStoreModel) { + makeAutoObservable( + this, + { + rootStore: false, + }, + {autoBind: true}, + ) + } + + get hasMore() { + return false + } + + get hasContent() { + return this.feeds.length > 0 + } + + get hasError() { + return this.error !== '' + } + + get isEmpty() { + return this.hasLoaded && !this.hasContent + } + + // public api + // = + + refresh = bundleAsync(async () => { + this._xLoading() + try { + const res = + await this.rootStore.agent.app.bsky.unspecced.getPopularFeedGenerators( + {}, + ) + this._replaceAll(res) + this._xIdle() + } catch (e: any) { + this._xIdle(e) + } + }) + + clear() { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = false + this.error = '' + this.feeds = [] + } + + // state transitions + // = + + _xLoading() { + this.isLoading = true + this.isRefreshing = true + this.error = '' + } + + _xIdle(err?: any) { + this.isLoading = false + this.isRefreshing = false + this.hasLoaded = true + this.error = cleanError(err) + if (err) { + this.rootStore.log.error('Failed to fetch popular feeds', err) + } + } + + // helper functions + // = + + _replaceAll(res: AppBskyUnspeccedGetPopularFeedGenerators.Response) { + this.feeds = [] + for (const f of res.data.feeds) { + this.feeds.push(new CustomFeedModel(this.rootStore, f)) + } + } +} diff --git a/src/view/com/feeds/SavedFeeds.tsx b/src/view/com/feeds/SavedFeeds.tsx index e92e741da..610562c9d 100644 --- a/src/view/com/feeds/SavedFeeds.tsx +++ b/src/view/com/feeds/SavedFeeds.tsx @@ -53,14 +53,28 @@ export const SavedFeeds = observer( const renderListFooterComponent = useCallback(() => { return ( <> - <Link - style={[styles.footerLink, pal.border]} - href="/settings/saved-feeds"> - <FontAwesomeIcon icon="cog" size={18} color={pal.colors.icon} /> - <Text type="lg-medium" style={pal.textLight}> - Change Order - </Text> - </Link> + <View style={[styles.footerLinks, pal.border]}> + <Link style={[styles.footerLink, pal.border]} href="/search/feeds"> + <FontAwesomeIcon + icon="search" + size={18} + color={pal.colors.icon} + /> + <Text type="lg-medium" style={pal.textLight}> + Discover new feeds + </Text> + </Link> + {!store.me.savedFeeds.isEmpty && ( + <Link + style={[styles.footerLink, pal.border]} + href="/settings/saved-feeds"> + <FontAwesomeIcon icon="cog" size={18} color={pal.colors.icon} /> + <Text type="lg-medium" style={pal.textLight}> + Change Order + </Text> + </Link> + )} + </View> <View style={[ pal.border, @@ -82,7 +96,7 @@ export const SavedFeeds = observer( </View> </> ) - }, [pal]) + }, [pal, store.me.savedFeeds.isEmpty]) const renderItem = useCallback( ({item}) => <CustomFeed key={item.data.uri} item={item} />, @@ -118,14 +132,16 @@ export const SavedFeeds = observer( ) const styles = StyleSheet.create({ + footerLinks: { + marginTop: 8, + borderBottomWidth: 1, + }, footerLink: { flexDirection: 'row', borderTopWidth: 1, - borderBottomWidth: 1, paddingHorizontal: 26, paddingVertical: 18, gap: 18, - marginTop: 8, }, empty: { paddingHorizontal: 18, @@ -134,7 +150,4 @@ const styles = StyleSheet.create({ marginHorizontal: 18, marginTop: 10, }, - feedItem: { - borderTopWidth: 1, - }, }) diff --git a/src/view/screens/CustomFeed.tsx b/src/view/screens/CustomFeed.tsx index 0690a17d8..49798d758 100644 --- a/src/view/screens/CustomFeed.tsx +++ b/src/view/screens/CustomFeed.tsx @@ -220,7 +220,7 @@ export const CustomFeedScreen = withAuthRequired( </Text> )} {isDesktopWeb && ( - <View style={styles.headerBtns}> + <View style={[styles.headerBtns, styles.headerBtnsDesktop]}> <Button type={currentFeed?.isSaved ? 'default' : 'inverted'} onPress={onToggleSaved} @@ -366,6 +366,10 @@ const styles = StyleSheet.create({ flexDirection: 'row', alignItems: 'center', }, + headerBtnsDesktop: { + marginTop: 8, + gap: 4, + }, headerAddBtn: { flexDirection: 'row', alignItems: 'center', diff --git a/src/view/screens/DiscoverFeeds.tsx b/src/view/screens/DiscoverFeeds.tsx new file mode 100644 index 000000000..82a37942f --- /dev/null +++ b/src/view/screens/DiscoverFeeds.tsx @@ -0,0 +1,109 @@ +import React from 'react' +import {RefreshControl, StyleSheet, View} from 'react-native' +import {observer} from 'mobx-react-lite' +import {useFocusEffect} from '@react-navigation/native' +import {NativeStackScreenProps, CommonNavigatorParams} from 'lib/routes/types' +import {withAuthRequired} from 'view/com/auth/withAuthRequired' +import {ViewHeader} from '../com/util/ViewHeader' +import {useStores} from 'state/index' +import {FeedsDiscoveryModel} from 'state/models/discovery/feeds' +import {CenteredView, FlatList} from 'view/com/util/Views' +import {CustomFeed} from 'view/com/feeds/CustomFeed' +import {Text} from 'view/com/util/text/Text' +import {isDesktopWeb} from 'platform/detection' +import {usePalette} from 'lib/hooks/usePalette' +import {s} from 'lib/styles' + +type Props = NativeStackScreenProps<CommonNavigatorParams, 'DiscoverFeeds'> +export const DiscoverFeedsScreen = withAuthRequired( + observer(({}: Props) => { + const store = useStores() + const pal = usePalette('default') + const feeds = React.useMemo(() => new FeedsDiscoveryModel(store), [store]) + + useFocusEffect( + React.useCallback(() => { + store.shell.setMinimalShellMode(false) + feeds.refresh() + }, [store, feeds]), + ) + + const onRefresh = React.useCallback(() => { + store.me.savedFeeds.refresh() + }, [store]) + + const renderListEmptyComponent = React.useCallback(() => { + return ( + <View + style={[ + pal.border, + !isDesktopWeb && s.flex1, + pal.viewLight, + styles.empty, + ]}> + <Text type="lg" style={[pal.text]}> + {feeds.isLoading + ? 'Loading...' + : `We can't find any feeds for some reason. This is probably an error - try refreshing!`} + </Text> + </View> + ) + }, [pal, feeds.isLoading]) + + const renderItem = React.useCallback( + ({item}) => ( + <CustomFeed + key={item.data.uri} + item={item} + showSaveBtn + showDescription + showLikes + /> + ), + [], + ) + + return ( + <CenteredView style={[styles.container, pal.view]}> + <View style={[isDesktopWeb && styles.containerDesktop, pal.border]}> + <ViewHeader title="Discover Feeds" showOnDesktop /> + </View> + <FlatList + style={[!isDesktopWeb && s.flex1]} + data={feeds.feeds} + keyExtractor={item => item.data.uri} + refreshControl={ + <RefreshControl + refreshing={feeds.isRefreshing} + onRefresh={onRefresh} + tintColor={pal.colors.text} + titleColor={pal.colors.text} + /> + } + renderItem={renderItem} + initialNumToRender={10} + ListEmptyComponent={renderListEmptyComponent} + extraData={feeds.isLoading} + /> + </CenteredView> + ) + }), +) + +const styles = StyleSheet.create({ + container: { + flex: 1, + paddingBottom: isDesktopWeb ? 0 : 100, + }, + containerDesktop: { + borderLeftWidth: 1, + borderRightWidth: 1, + }, + empty: { + paddingHorizontal: 18, + paddingVertical: 16, + borderRadius: 8, + marginHorizontal: 18, + marginTop: 10, + }, +}) |