diff --git a/src/lib/hooks/useDesktopRightNavItems.ts b/src/lib/hooks/useDesktopRightNavItems.ts deleted file mode 100644 index f27efd28..00000000 --- a/src/lib/hooks/useDesktopRightNavItems.ts +++ /dev/null @@ -1,51 +0,0 @@ -import {useEffect, useState} from 'react' -import {useStores} from 'state/index' -import isEqual from 'lodash.isequal' -import {AtUri} from '@atproto/api' -import {FeedSourceModel} from 'state/models/content/feed-source' - -interface RightNavItem { - uri: string - href: string - hostname: string - collection: string - rkey: string - displayName: string -} - -export function useDesktopRightNavItems(uris: string[]): RightNavItem[] { - const store = useStores() - const [items, setItems] = useState([]) - const [lastUris, setLastUris] = useState([]) - - useEffect(() => { - if (isEqual(uris, lastUris)) { - // no changes - return - } - - async function fetchFeedInfo() { - const models = uris - .slice(0, 25) - .map(uri => new FeedSourceModel(store, uri)) - await Promise.all(models.map(m => m.setup())) - setItems( - models.map(model => { - const {hostname, collection, rkey} = new AtUri(model.uri) - return { - uri: model.uri, - href: model.href, - hostname, - collection, - rkey, - displayName: model.displayName, - } - }), - ) - setLastUris(uris) - } - fetchFeedInfo() - }, [store, uris, lastUris, setLastUris, setItems]) - - return items -} diff --git a/src/lib/hooks/useHomeTabs.ts b/src/lib/hooks/useHomeTabs.ts deleted file mode 100644 index 69183e62..00000000 --- a/src/lib/hooks/useHomeTabs.ts +++ /dev/null @@ -1,29 +0,0 @@ -import {useEffect, useState} from 'react' -import {useStores} from 'state/index' -import isEqual from 'lodash.isequal' -import {FeedSourceModel} from 'state/models/content/feed-source' - -export function useHomeTabs(uris: string[]): string[] { - const store = useStores() - const [tabs, setTabs] = useState(['Following']) - const [lastUris, setLastUris] = useState([]) - - useEffect(() => { - if (isEqual(uris, lastUris)) { - // no changes - return - } - - async function fetchFeedInfo() { - const models = uris - .slice(0, 25) - .map(uri => new FeedSourceModel(store, uri)) - await Promise.all(models.map(m => m.setup())) - setTabs(['Following'].concat(models.map(f => f.displayName))) - setLastUris(uris) - } - fetchFeedInfo() - }, [store, uris, lastUris, setLastUris, setTabs]) - - return tabs -} diff --git a/src/state/models/content/feed-source.ts b/src/state/models/content/feed-source.ts index 156e3be3..cd8c08b5 100644 --- a/src/state/models/content/feed-source.ts +++ b/src/state/models/content/feed-source.ts @@ -61,7 +61,7 @@ export class FeedSourceModel { } get isPinned() { - return this.rootStore.preferences.isPinnedFeed(this.uri) + return false } get isLiked() { diff --git a/src/state/models/ui/preferences.ts b/src/state/models/ui/preferences.ts index 4f43487e..1068ac65 100644 --- a/src/state/models/ui/preferences.ts +++ b/src/state/models/ui/preferences.ts @@ -4,7 +4,6 @@ import { BskyFeedViewPreference, BskyThreadViewPreference, } from '@atproto/api' -import AwaitLock from 'await-lock' import {isObj, hasProp} from 'lib/type-guards' import {RootStoreModel} from '../root-store' import {ModerationOpts} from '@atproto/api' @@ -33,30 +32,17 @@ export class LabelPreferencesModel { } export class PreferencesModel { - adultContentEnabled = false contentLabels = new LabelPreferencesModel() savedFeeds: string[] = [] pinnedFeeds: string[] = [] - birthDate: Date | undefined = undefined - homeFeed: FeedViewPreference = { - hideReplies: false, - hideRepliesByUnfollowed: false, - hideRepliesByLikeCount: 0, - hideReposts: false, - hideQuotePosts: false, - lab_mergeFeedEnabled: false, // experimental - } thread: ThreadViewPreference = { sort: 'oldest', prioritizeFollowedUsers: true, lab_treeViewEnabled: false, // experimental } - // used to linearize async modifications to state - lock = new AwaitLock() - constructor(public rootStore: RootStoreModel) { - makeAutoObservable(this, {lock: false}, {autoBind: true}) + makeAutoObservable(this, {}, {autoBind: true}) } serialize() { @@ -106,7 +92,7 @@ export class PreferencesModel { get moderationOpts(): ModerationOpts { return { userDid: this.rootStore.session.currentSession?.did || '', - adultContentEnabled: this.adultContentEnabled, + adultContentEnabled: false, labels: { // TEMP translate old settings until this UI can be migrated -prf porn: tempfixLabelPref(this.contentLabels.nsfw), diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts index 5754d2c7..dde37315 100644 --- a/src/state/queries/feed.ts +++ b/src/state/queries/feed.ts @@ -1,9 +1,11 @@ +import React from 'react' import { useQuery, useInfiniteQuery, InfiniteData, QueryKey, useMutation, + useQueryClient, } from '@tanstack/react-query' import { AtUri, @@ -13,16 +15,22 @@ import { AppBskyUnspeccedGetPopularFeedGenerators, } from '@atproto/api' +import {router} from '#/routes' import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeHandle} from '#/lib/strings/handles' import {useSession} from '#/state/session' +import {usePreferencesQuery} from '#/state/queries/preferences' -type FeedSourceInfo = +export type FeedSourceInfo = | { type: 'feed' uri: string + route: { + href: string + name: string + params: Record + } cid: string - href: string avatar: string | undefined displayName: string description: RichText @@ -34,8 +42,12 @@ type FeedSourceInfo = | { type: 'list' uri: string + route: { + href: string + name: string + params: Record + } cid: string - href: string avatar: string | undefined displayName: string description: RichText @@ -43,7 +55,7 @@ type FeedSourceInfo = creatorHandle: string } -export const useFeedSourceInfoQueryKey = ({uri}: {uri: string}) => [ +export const feedSourceInfoQueryKey = ({uri}: {uri: string}) => [ 'getFeedSourceInfo', uri, ] @@ -53,19 +65,24 @@ const feedSourceNSIDs = { list: 'app.bsky.graph.list', } -function hydrateFeedGenerator( +export 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}` + const route = router.matchPath(href) return { type: 'feed', uri: view.uri, cid: view.cid, - href, + route: { + href, + name: route[0], + params: route[1], + }, avatar: view.avatar, displayName: view.displayName ? sanitizeDisplayName(view.displayName) @@ -81,17 +98,22 @@ function hydrateFeedGenerator( } } -function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo { +export 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}` + const route = router.matchPath(href) return { type: 'list', uri: view.uri, + route: { + href, + name: route[0], + params: route[1], + }, cid: view.cid, - href, avatar: view.avatar, description: new RichText({ text: view.description || '', @@ -105,13 +127,17 @@ function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo { } } +export function getFeedTypeFromUri(uri: string) { + const {pathname} = new AtUri(uri) + return pathname.includes(feedSourceNSIDs.feed) ? 'feed' : 'list' +} + export function useFeedSourceInfoQuery({uri}: {uri: string}) { const {agent} = useSession() - const {pathname} = new AtUri(uri) - const type = pathname.includes(feedSourceNSIDs.feed) ? 'feed' : 'list' + const type = getFeedTypeFromUri(uri) return useQuery({ - queryKey: useFeedSourceInfoQueryKey({uri}), + queryKey: feedSourceInfoQueryKey({uri}), queryFn: async () => { let view: FeedSourceInfo @@ -170,3 +196,87 @@ export function useSearchPopularFeedsMutation() { }, }) } + +const FOLLOWING_FEED_STUB: FeedSourceInfo = { + type: 'feed', + displayName: 'Following', + uri: '', + route: { + href: '/', + name: 'Home', + params: {}, + }, + cid: '', + avatar: '', + description: new RichText({text: ''}), + creatorDid: '', + creatorHandle: '', + likeCount: 0, + likeUri: '', +} + +export function usePinnedFeedsInfos(): FeedSourceInfo[] { + const {agent} = useSession() + const queryClient = useQueryClient() + const [tabs, setTabs] = React.useState([ + FOLLOWING_FEED_STUB, + ]) + const {data: preferences} = usePreferencesQuery() + const pinnedFeedsKey = JSON.stringify(preferences?.feeds?.pinned) + + React.useEffect(() => { + if (!preferences?.feeds?.pinned) return + const uris = preferences.feeds.pinned + + async function fetchFeedInfo() { + const reqs = [] + + for (const uri of uris) { + const cached = queryClient.getQueryData( + feedSourceInfoQueryKey({uri}), + ) + + if (cached) { + reqs.push(cached) + } else { + reqs.push( + queryClient.fetchQuery({ + queryKey: feedSourceInfoQueryKey({uri}), + queryFn: async () => { + const type = getFeedTypeFromUri(uri) + + if (type === 'feed') { + const res = await agent.app.bsky.feed.getFeedGenerator({ + feed: uri, + }) + return hydrateFeedGenerator(res.data.view) + } else { + const res = await agent.app.bsky.graph.getList({ + list: uri, + limit: 1, + }) + return hydrateList(res.data.list) + } + }, + }), + ) + } + } + + const views = await Promise.all(reqs) + + setTabs([FOLLOWING_FEED_STUB].concat(views)) + } + + fetchFeedInfo() + }, [ + agent, + queryClient, + setTabs, + preferences?.feeds?.pinned, + // ensure we react to re-ordering + pinnedFeedsKey, + ]) + + return tabs +} diff --git a/src/state/queries/preferences/types.ts b/src/state/queries/preferences/types.ts index 9f4c30e5..2b04b725 100644 --- a/src/state/queries/preferences/types.ts +++ b/src/state/queries/preferences/types.ts @@ -2,6 +2,7 @@ import { BskyPreferences, LabelPreference, BskyThreadViewPreference, + BskyFeedViewPreference, } from '@atproto/api' export type ConfigurableLabelGroup = @@ -29,7 +30,9 @@ export type UsePreferencesQueryResponse = Omit< * we clean up the data in `usePreferencesQuery`. */ contentLabels: Record - feedViewPrefs: BskyPreferences['feedViewPrefs']['home'] + feedViewPrefs: BskyFeedViewPreference & { + lab_mergeFeedEnabled: boolean + } /** * User thread-view prefs, including newer fields that may not be typed yet. */ diff --git a/src/view/com/modals/ContentFilteringSettings.tsx b/src/view/com/modals/ContentFilteringSettings.tsx index cd539406..8dc3311f 100644 --- a/src/view/com/modals/ContentFilteringSettings.tsx +++ b/src/view/com/modals/ContentFilteringSettings.tsx @@ -1,5 +1,5 @@ import React from 'react' -import {BskyPreferences, LabelPreference} from '@atproto/api' +import {LabelPreference} from '@atproto/api' import {StyleSheet, Pressable, View} from 'react-native' import LinearGradient from 'react-native-linear-gradient' import {observer} from 'mobx-react-lite' @@ -23,6 +23,7 @@ import { usePreferencesSetAdultContentMutation, ConfigurableLabelGroup, CONFIGURABLE_LABEL_GROUPS, + UsePreferencesQueryResponse, } from '#/state/queries/preferences' export const snapPoints = ['90%'] @@ -175,7 +176,7 @@ const ContentLabelPref = observer(function ContentLabelPrefImpl({ labelGroup, disabled, }: { - preferences?: BskyPreferences + preferences?: UsePreferencesQueryResponse labelGroup: ConfigurableLabelGroup disabled?: boolean }) { diff --git a/src/view/com/pager/FeedsTabBar.web.tsx b/src/view/com/pager/FeedsTabBar.web.tsx index 296af76e..b48690a6 100644 --- a/src/view/com/pager/FeedsTabBar.web.tsx +++ b/src/view/com/pager/FeedsTabBar.web.tsx @@ -4,13 +4,12 @@ import Animated from 'react-native-reanimated' import {observer} from 'mobx-react-lite' import {TabBar} from 'view/com/pager/TabBar' import {RenderTabBarFnProps} from 'view/com/pager/Pager' -import {useStores} from 'state/index' -import {useHomeTabs} from 'lib/hooks/useHomeTabs' import {usePalette} from 'lib/hooks/usePalette' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {FeedsTabBar as FeedsTabBarMobile} from './FeedsTabBarMobile' import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useShellLayout} from '#/state/shell/shell-layout' +import {usePinnedFeedsInfos} from '#/state/queries/feed' export const FeedsTabBar = observer(function FeedsTabBarImpl( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, @@ -28,11 +27,11 @@ export const FeedsTabBar = observer(function FeedsTabBarImpl( const FeedsTabBarTablet = observer(function FeedsTabBarTabletImpl( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { - const store = useStores() - const items = useHomeTabs(store.preferences.pinnedFeeds) + const feeds = usePinnedFeedsInfos() const pal = usePalette('default') const {headerMinimalShellTransform} = useMinimalShellMode() const {headerHeight} = useShellLayout() + const items = feeds.map(f => f.displayName) return ( // @ts-ignore the type signature for transform wrong here, translateX and translateY need to be in separate objects -prf diff --git a/src/view/com/pager/FeedsTabBarMobile.tsx b/src/view/com/pager/FeedsTabBarMobile.tsx index d79bfe94..de985fb7 100644 --- a/src/view/com/pager/FeedsTabBarMobile.tsx +++ b/src/view/com/pager/FeedsTabBarMobile.tsx @@ -3,8 +3,6 @@ import {StyleSheet, TouchableOpacity, View} from 'react-native' import {observer} from 'mobx-react-lite' import {TabBar} from 'view/com/pager/TabBar' import {RenderTabBarFnProps} from 'view/com/pager/Pager' -import {useStores} from 'state/index' -import {useHomeTabs} from 'lib/hooks/useHomeTabs' import {usePalette} from 'lib/hooks/usePalette' import {useColorSchemeStyle} from 'lib/hooks/useColorSchemeStyle' import {Link} from '../util/Link' @@ -20,19 +18,20 @@ import {useMinimalShellMode} from 'lib/hooks/useMinimalShellMode' import {useSetDrawerOpen} from '#/state/shell/drawer-open' import {useShellLayout} from '#/state/shell/shell-layout' import {useSession} from '#/state/session' +import {usePinnedFeedsInfos} from '#/state/queries/feed' export const FeedsTabBar = observer(function FeedsTabBarImpl( props: RenderTabBarFnProps & {testID?: string; onPressSelected: () => void}, ) { const pal = usePalette('default') - const store = useStores() const {isSandbox} = useSession() const {_} = useLingui() const setDrawerOpen = useSetDrawerOpen() - const items = useHomeTabs(store.preferences.pinnedFeeds) + const feeds = usePinnedFeedsInfos() const brandBlue = useColorSchemeStyle(s.brandBlue, s.blue3) const {headerHeight} = useShellLayout() const {headerMinimalShellTransform} = useMinimalShellMode() + const items = feeds.map(f => f.displayName) const onPressAvi = React.useCallback(() => { setDrawerOpen(true) diff --git a/src/view/screens/Feeds.tsx b/src/view/screens/Feeds.tsx index c78f44cd..5f60322b 100644 --- a/src/view/screens/Feeds.tsx +++ b/src/view/screens/Feeds.tsx @@ -508,7 +508,7 @@ function SavedFeed({feedUri}: {feedUri: string}) { return ( export const HomeScreen = withAuthRequired( @@ -23,19 +23,15 @@ export const HomeScreen = withAuthRequired( const pagerRef = React.useRef(null) const [selectedPage, setSelectedPage] = React.useState(0) const [customFeeds, setCustomFeeds] = React.useState([]) - const [requestedCustomFeeds, setRequestedCustomFeeds] = React.useState< - string[] - >([]) + const {data: preferences} = usePreferencesQuery() React.useEffect(() => { - const pinned = store.preferences.pinnedFeeds + if (!preferences?.feeds?.pinned) return - if (isEqual(pinned, requestedCustomFeeds)) { - // no changes - return - } + const pinned = preferences.feeds.pinned const feeds: FeedDescriptor[] = [] + for (const uri of pinned) { if (uri.includes('app.bsky.feed.generator')) { feeds.push(`feedgen|${uri}`) @@ -43,31 +39,20 @@ export const HomeScreen = withAuthRequired( feeds.push(`list|${uri}`) } } - pagerRef.current?.setPage(0) + setCustomFeeds(feeds) - setRequestedCustomFeeds(pinned) - }, [ - store, - store.preferences.pinnedFeeds, - customFeeds, - setCustomFeeds, - pagerRef, - requestedCustomFeeds, - setRequestedCustomFeeds, - ]) + + pagerRef.current?.setPage(0) + }, [preferences?.feeds?.pinned, setCustomFeeds, pagerRef]) const homeFeedParams = React.useMemo(() => { - if (!store.preferences.homeFeed.lab_mergeFeedEnabled) { - return {} - } + if (!preferences) return {} + return { - mergeFeedEnabled: true, - mergeFeedSources: store.preferences.savedFeeds, + mergeFeedEnabled: preferences.feedViewPrefs.lab_mergeFeedEnabled, + mergeFeedSources: preferences.feeds.saved, } - }, [ - store.preferences.homeFeed.lab_mergeFeedEnabled, - store.preferences.savedFeeds, - ]) + }, [preferences]) useFocusEffect( React.useCallback(() => { diff --git a/src/view/shell/desktop/Feeds.tsx b/src/view/shell/desktop/Feeds.tsx index 3237d2cd..9cb10517 100644 --- a/src/view/shell/desktop/Feeds.tsx +++ b/src/view/shell/desktop/Feeds.tsx @@ -2,16 +2,14 @@ import React from 'react' import {View, StyleSheet} from 'react-native' import {useNavigationState} from '@react-navigation/native' import {observer} from 'mobx-react-lite' -import {useStores} from 'state/index' import {usePalette} from 'lib/hooks/usePalette' -import {useDesktopRightNavItems} from 'lib/hooks/useDesktopRightNavItems' import {TextLink} from 'view/com/util/Link' import {getCurrentRoute} from 'lib/routes/helpers' +import {usePinnedFeedsInfos} from '#/state/queries/feed' export const DesktopFeeds = observer(function DesktopFeeds() { - const store = useStores() const pal = usePalette('default') - const items = useDesktopRightNavItems(store.preferences.pinnedFeeds) + const feeds = usePinnedFeedsInfos() const route = useNavigationState(state => { if (!state) { @@ -23,29 +21,29 @@ export const DesktopFeeds = observer(function DesktopFeeds() { return ( - {items.map(item => { - try { - const params = route.params as Record - const routeName = - item.collection === 'app.bsky.feed.generator' - ? 'ProfileFeed' - : 'ProfileList' - return ( - - ) - } catch { - return null - } - })} + {feeds + .filter(f => f.displayName !== 'Following') + .map(feed => { + try { + const params = route.params as Record + const routeName = + feed.type === 'feed' ? 'ProfileFeed' : 'ProfileList' + return ( + + ) + } catch { + return null + } + })}