about summary refs log tree commit diff
path: root/src/state
diff options
context:
space:
mode:
Diffstat (limited to 'src/state')
-rw-r--r--src/state/preferences/feed-tuners.tsx5
-rw-r--r--src/state/queries/feed.ts101
-rw-r--r--src/state/queries/post-feed.ts6
-rw-r--r--src/state/queries/preferences/const.ts17
-rw-r--r--src/state/queries/preferences/index.ts71
-rw-r--r--src/state/queries/preferences/types.ts5
-rw-r--r--src/state/session/agent.ts34
-rw-r--r--src/state/shell/selected-feed.tsx35
8 files changed, 147 insertions, 127 deletions
diff --git a/src/state/preferences/feed-tuners.tsx b/src/state/preferences/feed-tuners.tsx
index c4954d20a..ac129d172 100644
--- a/src/state/preferences/feed-tuners.tsx
+++ b/src/state/preferences/feed-tuners.tsx
@@ -1,9 +1,10 @@
 import {useMemo} from 'react'
+
 import {FeedTuner} from '#/lib/api/feed-manip'
 import {FeedDescriptor} from '../queries/post-feed'
-import {useLanguagePrefs} from './languages'
 import {usePreferencesQuery} from '../queries/preferences'
 import {useSession} from '../session'
+import {useLanguagePrefs} from './languages'
 
 export function useFeedTuners(feedDesc: FeedDescriptor) {
   const langPrefs = useLanguagePrefs()
@@ -20,7 +21,7 @@ export function useFeedTuners(feedDesc: FeedDescriptor) {
     if (feedDesc.startsWith('list')) {
       return [FeedTuner.dedupReposts]
     }
-    if (feedDesc === 'home' || feedDesc === 'following') {
+    if (feedDesc === 'following') {
       const feedTuners = []
 
       if (preferences?.feedViewPrefs.hideReposts) {
diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts
index 1741d113c..19cded087 100644
--- a/src/state/queries/feed.ts
+++ b/src/state/queries/feed.ts
@@ -1,4 +1,5 @@
 import {
+  AppBskyActorDefs,
   AppBskyFeedDefs,
   AppBskyGraphDefs,
   AppBskyUnspeccedGetPopularFeedGenerators,
@@ -13,16 +14,19 @@ import {
   useQuery,
 } from '@tanstack/react-query'
 
+import {DISCOVER_FEED_URI, DISCOVER_SAVED_FEED} from '#/lib/constants'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {STALE} from '#/state/queries'
 import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useAgent, useSession} from '#/state/session'
 import {router} from '#/routes'
+import {FeedDescriptor} from './post-feed'
 
 export type FeedSourceFeedInfo = {
   type: 'feed'
   uri: string
+  feedDescriptor: FeedDescriptor
   route: {
     href: string
     name: string
@@ -41,6 +45,7 @@ export type FeedSourceFeedInfo = {
 export type FeedSourceListInfo = {
   type: 'list'
   uri: string
+  feedDescriptor: FeedDescriptor
   route: {
     href: string
     name: string
@@ -79,6 +84,7 @@ export function hydrateFeedGenerator(
   return {
     type: 'feed',
     uri: view.uri,
+    feedDescriptor: `feedgen|${view.uri}`,
     cid: view.cid,
     route: {
       href,
@@ -110,6 +116,7 @@ export function hydrateList(view: AppBskyGraphDefs.ListView): FeedSourceInfo {
   return {
     type: 'list',
     uri: view.uri,
+    feedDescriptor: `list|${view.uri}`,
     route: {
       href,
       name: route[0],
@@ -202,27 +209,15 @@ 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 type SavedFeedSourceInfo = FeedSourceInfo & {
+  savedFeed: AppBskyActorDefs.SavedFeed
 }
-const DISCOVER_FEED_STUB: FeedSourceInfo = {
+
+const PWI_DISCOVER_FEED_STUB: SavedFeedSourceInfo = {
   type: 'feed',
   displayName: 'Discover',
-  uri: '',
+  uri: DISCOVER_FEED_URI,
+  feedDescriptor: `feedgen|${DISCOVER_FEED_URI}`,
   route: {
     href: '/',
     name: 'Home',
@@ -235,6 +230,11 @@ const DISCOVER_FEED_STUB: FeedSourceInfo = {
   creatorHandle: '',
   likeCount: 0,
   likeUri: '',
+  // ---
+  savedFeed: {
+    id: 'pwi-discover',
+    ...DISCOVER_SAVED_FEED,
+  },
 }
 
 const pinnedFeedInfosQueryKeyRoot = 'pinnedFeedsInfos'
@@ -243,43 +243,45 @@ export function usePinnedFeedsInfos() {
   const {hasSession} = useSession()
   const {getAgent} = useAgent()
   const {data: preferences, isLoading: isLoadingPrefs} = usePreferencesQuery()
-  const pinnedUris = preferences?.feeds?.pinned ?? []
+  const pinnedItems = preferences?.savedFeeds.filter(feed => feed.pinned) ?? []
 
   return useQuery({
     staleTime: STALE.INFINITY,
     enabled: !isLoadingPrefs,
     queryKey: [
       pinnedFeedInfosQueryKeyRoot,
-      (hasSession ? 'authed:' : 'unauthed:') + pinnedUris.join(','),
+      (hasSession ? 'authed:' : 'unauthed:') +
+        pinnedItems.map(f => f.value).join(','),
     ],
     queryFn: async () => {
-      let resolved = new Map()
+      if (!hasSession) {
+        return [PWI_DISCOVER_FEED_STUB]
+      }
+
+      let resolved = new Map<string, FeedSourceInfo>()
 
       // Get all feeds. We can do this in a batch.
-      const feedUris = pinnedUris.filter(
-        uri => getFeedTypeFromUri(uri) === 'feed',
-      )
+      const pinnedFeeds = pinnedItems.filter(feed => feed.type === 'feed')
       let feedsPromise = Promise.resolve()
-      if (feedUris.length > 0) {
+      if (pinnedFeeds.length > 0) {
         feedsPromise = getAgent()
           .app.bsky.feed.getFeedGenerators({
-            feeds: feedUris,
+            feeds: pinnedFeeds.map(f => f.value),
           })
           .then(res => {
-            for (let feedView of res.data.feeds) {
+            for (let i = 0; i < res.data.feeds.length; i++) {
+              const feedView = res.data.feeds[i]
               resolved.set(feedView.uri, hydrateFeedGenerator(feedView))
             }
           })
       }
 
       // Get all lists. This currently has to be done individually.
-      const listUris = pinnedUris.filter(
-        uri => getFeedTypeFromUri(uri) === 'list',
-      )
-      const listsPromises = listUris.map(listUri =>
+      const pinnedLists = pinnedItems.filter(feed => feed.type === 'list')
+      const listsPromises = pinnedLists.map(list =>
         getAgent()
           .app.bsky.graph.getList({
-            list: listUri,
+            list: list.value,
             limit: 1,
           })
           .then(res => {
@@ -288,12 +290,37 @@ export function usePinnedFeedsInfos() {
           }),
       )
 
-      // The returned result will have the original order.
-      const result = [hasSession ? FOLLOWING_FEED_STUB : DISCOVER_FEED_STUB]
       await Promise.allSettled([feedsPromise, ...listsPromises])
-      for (let pinnedUri of pinnedUris) {
-        if (resolved.has(pinnedUri)) {
-          result.push(resolved.get(pinnedUri))
+
+      // order the feeds/lists in the order they were pinned
+      const result: SavedFeedSourceInfo[] = []
+      for (let pinnedItem of pinnedItems) {
+        const feedInfo = resolved.get(pinnedItem.value)
+        if (feedInfo) {
+          result.push({
+            ...feedInfo,
+            savedFeed: pinnedItem,
+          })
+        } else if (pinnedItem.type === 'timeline') {
+          result.push({
+            type: 'feed',
+            displayName: 'Following',
+            uri: pinnedItem.value,
+            feedDescriptor: 'following',
+            route: {
+              href: '/',
+              name: 'Home',
+              params: {},
+            },
+            cid: '',
+            avatar: '',
+            description: new RichText({text: ''}),
+            creatorDid: '',
+            creatorHandle: '',
+            likeCount: 0,
+            likeUri: '',
+            savedFeed: pinnedItem,
+          })
         }
       }
       return result
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index dc86a9ba0..7b312edfe 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -44,8 +44,8 @@ type AuthorFilter =
   | 'posts_with_media'
 type FeedUri = string
 type ListUri = string
+
 export type FeedDescriptor =
-  | 'home'
   | 'following'
   | `author|${ActorDid}|${AuthorFilter}`
   | `feedgen|${FeedUri}`
@@ -390,7 +390,7 @@ function createApi({
   userInterests?: string
   getAgent: () => BskyAgent
 }) {
-  if (feedDesc === 'home') {
+  if (feedDesc === 'following') {
     if (feedParams.mergeFeedEnabled) {
       return new MergeFeedAPI({
         getAgent,
@@ -401,8 +401,6 @@ function createApi({
     } else {
       return new HomeFeedAPI({getAgent, userInterests})
     }
-  } else if (feedDesc === 'following') {
-    return new FollowingFeedAPI({getAgent})
   } else if (feedDesc.startsWith('author')) {
     const [_, actor, filter] = feedDesc.split('|')
     return new AuthorFeedAPI({getAgent, feedParams: {actor, filter}})
diff --git a/src/state/queries/preferences/const.ts b/src/state/queries/preferences/const.ts
index 4cb4d1e96..d94edb47e 100644
--- a/src/state/queries/preferences/const.ts
+++ b/src/state/queries/preferences/const.ts
@@ -1,8 +1,8 @@
+import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
 import {
-  UsePreferencesQueryResponse,
   ThreadViewPreferences,
+  UsePreferencesQueryResponse,
 } from '#/state/queries/preferences/types'
-import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
 
 export const DEFAULT_HOME_FEED_PREFS: UsePreferencesQueryResponse['feedViewPrefs'] =
   {
@@ -20,20 +20,8 @@ export const DEFAULT_THREAD_VIEW_PREFS: ThreadViewPreferences = {
   lab_treeViewEnabled: false,
 }
 
-const DEFAULT_PROD_FEED_PREFIX = (rkey: string) =>
-  `at://did:plc:z72i7hdynmk6r22z27h6tvur/app.bsky.feed.generator/${rkey}`
-export const DEFAULT_PROD_FEEDS = {
-  pinned: [DEFAULT_PROD_FEED_PREFIX('whats-hot')],
-  saved: [DEFAULT_PROD_FEED_PREFIX('whats-hot')],
-}
-
 export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
   birthDate: new Date('2022-11-17'), // TODO(pwi)
-  feeds: {
-    saved: [],
-    pinned: [],
-    unpinned: [],
-  },
   moderationPrefs: {
     adultContentEnabled: false,
     labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
@@ -45,4 +33,5 @@ export const DEFAULT_LOGGED_OUT_PREFERENCES: UsePreferencesQueryResponse = {
   threadViewPrefs: DEFAULT_THREAD_VIEW_PREFS,
   userAge: 13, // TODO(pwi)
   interests: {tags: []},
+  savedFeeds: [],
 }
diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts
index f51eaac2a..b3d2fa9ec 100644
--- a/src/state/queries/preferences/index.ts
+++ b/src/state/queries/preferences/index.ts
@@ -51,14 +51,11 @@ export function usePreferencesQuery() {
 
         const preferences: UsePreferencesQueryResponse = {
           ...res,
-          feeds: {
-            saved: res.feeds?.saved || [],
-            pinned: res.feeds?.pinned || [],
-            unpinned:
-              res.feeds.saved?.filter(f => {
-                return !res.feeds.pinned?.includes(f)
-              }) || [],
-          },
+          savedFeeds: res.savedFeeds.filter(f => f.type !== 'unknown'),
+          /**
+           * Special preference, only used for following feed, previously
+           * called `home`
+           */
           feedViewPrefs: {
             ...DEFAULT_HOME_FEED_PREFS,
             ...(res.feedViewPrefs.home || {}),
@@ -168,6 +165,10 @@ export function useSetFeedViewPreferencesMutation() {
 
   return useMutation<void, unknown, Partial<BskyFeedViewPreference>>({
     mutationFn: async prefs => {
+      /*
+       * special handling here, merged into `feedViewPrefs` above, since
+       * following was previously called `home`
+       */
       await getAgent().setFeedViewPrefs('home', prefs)
       // triggers a refetch
       await queryClient.invalidateQueries({
@@ -192,17 +193,13 @@ export function useSetThreadViewPreferencesMutation() {
   })
 }
 
-export function useSetSaveFeedsMutation() {
+export function useOverwriteSavedFeedsMutation() {
   const queryClient = useQueryClient()
   const {getAgent} = useAgent()
 
-  return useMutation<
-    void,
-    unknown,
-    Pick<UsePreferencesQueryResponse['feeds'], 'saved' | 'pinned'>
-  >({
-    mutationFn: async ({saved, pinned}) => {
-      await getAgent().setSavedFeeds(saved, pinned)
+  return useMutation<void, unknown, AppBskyActorDefs.SavedFeed[]>({
+    mutationFn: async savedFeeds => {
+      await getAgent().overwriteSavedFeeds(savedFeeds)
       // triggers a refetch
       await queryClient.invalidateQueries({
         queryKey: preferencesQueryKey,
@@ -211,13 +208,17 @@ export function useSetSaveFeedsMutation() {
   })
 }
 
-export function useSaveFeedMutation() {
+export function useAddSavedFeedsMutation() {
   const queryClient = useQueryClient()
   const {getAgent} = useAgent()
 
-  return useMutation<void, unknown, {uri: string}>({
-    mutationFn: async ({uri}) => {
-      await getAgent().addSavedFeed(uri)
+  return useMutation<
+    void,
+    unknown,
+    Pick<AppBskyActorDefs.SavedFeed, 'type' | 'value' | 'pinned'>[]
+  >({
+    mutationFn: async savedFeeds => {
+      await getAgent().addSavedFeeds(savedFeeds)
       track('CustomFeed:Save')
       // triggers a refetch
       await queryClient.invalidateQueries({
@@ -231,9 +232,9 @@ export function useRemoveFeedMutation() {
   const queryClient = useQueryClient()
   const {getAgent} = useAgent()
 
-  return useMutation<void, unknown, {uri: string}>({
-    mutationFn: async ({uri}) => {
-      await getAgent().removeSavedFeed(uri)
+  return useMutation<void, unknown, Pick<AppBskyActorDefs.SavedFeed, 'id'>>({
+    mutationFn: async savedFeed => {
+      await getAgent().removeSavedFeeds([savedFeed.id])
       track('CustomFeed:Unsave')
       // triggers a refetch
       await queryClient.invalidateQueries({
@@ -243,30 +244,14 @@ export function useRemoveFeedMutation() {
   })
 }
 
-export function usePinFeedMutation() {
+export function useUpdateSavedFeedsMutation() {
   const queryClient = useQueryClient()
   const {getAgent} = useAgent()
 
-  return useMutation<void, unknown, {uri: string}>({
-    mutationFn: async ({uri}) => {
-      await getAgent().addPinnedFeed(uri)
-      track('CustomFeed:Pin', {uri})
-      // triggers a refetch
-      await queryClient.invalidateQueries({
-        queryKey: preferencesQueryKey,
-      })
-    },
-  })
-}
-
-export function useUnpinFeedMutation() {
-  const queryClient = useQueryClient()
-  const {getAgent} = useAgent()
+  return useMutation<void, unknown, AppBskyActorDefs.SavedFeed[]>({
+    mutationFn: async feeds => {
+      await getAgent().updateSavedFeeds(feeds)
 
-  return useMutation<void, unknown, {uri: string}>({
-    mutationFn: async ({uri}) => {
-      await getAgent().removePinnedFeed(uri)
-      track('CustomFeed:Unpin', {uri})
       // triggers a refetch
       await queryClient.invalidateQueries({
         queryKey: preferencesQueryKey,
diff --git a/src/state/queries/preferences/types.ts b/src/state/queries/preferences/types.ts
index 96da16f1a..928bb90da 100644
--- a/src/state/queries/preferences/types.ts
+++ b/src/state/queries/preferences/types.ts
@@ -1,7 +1,7 @@
 import {
+  BskyFeedViewPreference,
   BskyPreferences,
   BskyThreadViewPreference,
-  BskyFeedViewPreference,
 } from '@atproto/api'
 
 export type UsePreferencesQueryResponse = Omit<
@@ -16,9 +16,6 @@ export type UsePreferencesQueryResponse = Omit<
    */
   threadViewPrefs: ThreadViewPreferences
   userAge: number | undefined
-  feeds: Required<BskyPreferences['feeds']> & {
-    unpinned: string[]
-  }
 }
 
 export type ThreadViewPreferences = Pick<
diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts
index 024f6e7d1..9633dc0e3 100644
--- a/src/state/session/agent.ts
+++ b/src/state/session/agent.ts
@@ -1,10 +1,15 @@
 import {AtpSessionData, AtpSessionEvent, BskyAgent} from '@atproto/api'
+import {TID} from '@atproto/common-web'
 
 import {networkRetry} from '#/lib/async/retry'
-import {PUBLIC_BSKY_SERVICE} from '#/lib/constants'
-import {IS_PROD_SERVICE} from '#/lib/constants'
+import {
+  DISCOVER_SAVED_FEED,
+  IS_PROD_SERVICE,
+  PUBLIC_BSKY_SERVICE,
+  TIMELINE_SAVED_FEED,
+} from '#/lib/constants'
 import {tryFetchGates} from '#/lib/statsig/statsig'
-import {DEFAULT_PROD_FEEDS} from '../queries/preferences'
+import {logger} from '#/logger'
 import {
   configureModerationForAccount,
   configureModerationForGuest,
@@ -134,9 +139,28 @@ export async function createAgentAndCreateAccount(
 
   // Not awaited so that we can still get into onboarding.
   // This is OK because we won't let you toggle adult stuff until you set the date.
-  agent.setPersonalDetails({birthDate: birthDate.toISOString()})
   if (IS_PROD_SERVICE(service)) {
-    agent.setSavedFeeds(DEFAULT_PROD_FEEDS.saved, DEFAULT_PROD_FEEDS.pinned)
+    try {
+      networkRetry(1, async () => {
+        await agent.setPersonalDetails({birthDate: birthDate.toISOString()})
+        await agent.overwriteSavedFeeds([
+          {
+            ...DISCOVER_SAVED_FEED,
+            id: TID.nextStr(),
+          },
+          {
+            ...TIMELINE_SAVED_FEED,
+            id: TID.nextStr(),
+          },
+        ])
+      })
+    } catch (e: any) {
+      logger.error(e, {
+        context: `session: createAgentAndCreateAccount failed to save personal details and feeds`,
+      })
+    }
+  } else {
+    agent.setPersonalDetails({birthDate: birthDate.toISOString()})
   }
 
   return prepareAgent(agent, gates, moderation, onSessionChange)
diff --git a/src/state/shell/selected-feed.tsx b/src/state/shell/selected-feed.tsx
index df50b3952..08b7ba77c 100644
--- a/src/state/shell/selected-feed.tsx
+++ b/src/state/shell/selected-feed.tsx
@@ -1,47 +1,46 @@
 import React from 'react'
 
-import {Gate} from '#/lib/statsig/gates'
-import {useGate} from '#/lib/statsig/statsig'
 import {isWeb} from '#/platform/detection'
 import * as persisted from '#/state/persisted'
+import {FeedDescriptor} from '#/state/queries/post-feed'
 
-type StateContext = string
-type SetContext = (v: string) => void
+type StateContext = FeedDescriptor | null
+type SetContext = (v: FeedDescriptor) => void
 
-const stateContext = React.createContext<StateContext>('home')
+const stateContext = React.createContext<StateContext>(null)
 const setContext = React.createContext<SetContext>((_: string) => {})
 
-function getInitialFeed(gate: (gateName: Gate) => boolean) {
+function getInitialFeed(): FeedDescriptor | null {
   if (isWeb) {
     if (window.location.pathname === '/') {
       const params = new URLSearchParams(window.location.search)
       const feedFromUrl = params.get('feed')
       if (feedFromUrl) {
         // If explicitly booted from a link like /?feed=..., prefer that.
-        return feedFromUrl
+        return feedFromUrl as FeedDescriptor
       }
     }
+
     const feedFromSession = sessionStorage.getItem('lastSelectedHomeFeed')
     if (feedFromSession) {
       // Fall back to a previously chosen feed for this browser tab.
-      return feedFromSession
+      return feedFromSession as FeedDescriptor
     }
   }
-  if (!gate('start_session_with_following_v2')) {
-    const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
-    if (feedFromPersisted) {
-      // Fall back to the last chosen one across all tabs.
-      return feedFromPersisted
-    }
+
+  const feedFromPersisted = persisted.get('lastSelectedHomeFeed')
+  if (feedFromPersisted) {
+    // Fall back to the last chosen one across all tabs.
+    return feedFromPersisted as FeedDescriptor
   }
-  return 'home'
+
+  return null
 }
 
 export function Provider({children}: React.PropsWithChildren<{}>) {
-  const gate = useGate()
-  const [state, setState] = React.useState(() => getInitialFeed(gate))
+  const [state, setState] = React.useState(() => getInitialFeed())
 
-  const saveState = React.useCallback((feed: string) => {
+  const saveState = React.useCallback((feed: FeedDescriptor) => {
     setState(feed)
     if (isWeb) {
       try {