diff options
author | Paul Frazee <pfrazee@gmail.com> | 2023-05-24 22:09:39 -0500 |
---|---|---|
committer | Paul Frazee <pfrazee@gmail.com> | 2023-05-24 22:09:39 -0500 |
commit | dfb39e7c4fcaff3effcc82b412191177fdfdaf22 (patch) | |
tree | 0771c5794290cdc29c5689cdd67384aadddcadcb | |
parent | 12c7f6d6a5f81570950c7ce84976250b28ef666b (diff) | |
download | voidsky-dfb39e7c4fcaff3effcc82b412191177fdfdaf22.tar.zst |
Add feed discovery page
-rw-r--r-- | bskyweb/cmd/bskyweb/server.go | 1 | ||||
-rw-r--r-- | package.json | 2 | ||||
-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 | ||||
-rw-r--r-- | yarn.lock | 8 |
10 files changed, 252 insertions, 20 deletions
diff --git a/bskyweb/cmd/bskyweb/server.go b/bskyweb/cmd/bskyweb/server.go index 07df85146..462740f54 100644 --- a/bskyweb/cmd/bskyweb/server.go +++ b/bskyweb/cmd/bskyweb/server.go @@ -105,6 +105,7 @@ func serve(cctx *cli.Context) error { // generic routes e.GET("/search", server.WebGeneric) + e.GET("/search/feeds", server.WebGeneric) e.GET("/notifications", server.WebGeneric) e.GET("/moderation", server.WebGeneric) e.GET("/moderation/mute-lists", server.WebGeneric) diff --git a/package.json b/package.json index 5c30a0cae..253c3b782 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,7 @@ "e2e:run": "detox test --configuration ios.sim.debug --take-screenshots all" }, "dependencies": { - "@atproto/api": "0.3.7", + "@atproto/api": "0.3.8", "@bam.tech/react-native-image-resizer": "^3.0.4", "@braintree/sanitize-url": "^6.0.2", "@expo/webpack-config": "^18.0.1", 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, + }, +}) diff --git a/yarn.lock b/yarn.lock index d42660eda..f20c29dc3 100644 --- a/yarn.lock +++ b/yarn.lock @@ -40,10 +40,10 @@ tlds "^1.234.0" typed-emitter "^2.1.0" -"@atproto/api@0.3.7": - version "0.3.7" - resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.7.tgz#5cc4b0ccc5c6690eb0e5a3ae138a84ce20697e2f" - integrity sha512-JHN3rHNGro4AaJWU64hsmpTUzd2+FbfMBiDkqyBmoKtj972ueBJeH8tz6WdnPcsIRfCj1kRthKFj2yJwgt6aSQ== +"@atproto/api@0.3.8": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@atproto/api/-/api-0.3.8.tgz#3fc0ebd092cc212c2d0b31a600fe1945a02f9cf7" + integrity sha512-7qaIZGEP5J9FW4z8bXezzAmLRzHSXXHo6bWP9Jyu5MLp8tYt9vG6yR2N0QA7GvO0xSYqP87Q5vblPjYXGqtDKg== dependencies: "@atproto/common-web" "*" "@atproto/uri" "*" |