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/feed-feedback.tsx98
-rw-r--r--src/state/messages/convo/agent.ts2
-rw-r--r--src/state/messages/events/agent.ts2
-rw-r--r--src/state/modals/index.tsx10
-rw-r--r--src/state/queries/actor-starter-packs.ts57
-rw-r--r--src/state/queries/feed.ts31
-rw-r--r--src/state/queries/handle-availability.ts7
-rw-r--r--src/state/queries/messages/accept-conversation.ts7
-rw-r--r--src/state/queries/messages/const.ts5
-rw-r--r--src/state/queries/messages/conversation.ts8
-rw-r--r--src/state/queries/messages/get-convo-availability.ts2
-rw-r--r--src/state/queries/messages/get-convo-for-members.ts4
-rw-r--r--src/state/queries/messages/leave-conversation.ts7
-rw-r--r--src/state/queries/messages/list-conversations.tsx2
-rw-r--r--src/state/queries/messages/mute-conversation.ts14
-rw-r--r--src/state/queries/messages/update-all-read.ts4
-rw-r--r--src/state/queries/service.ts5
-rw-r--r--src/state/queries/trending/useGetSuggestedUsersQuery.ts9
-rw-r--r--src/state/session/agent.ts44
-rw-r--r--src/state/unstable-post-source.tsx4
20 files changed, 259 insertions, 63 deletions
diff --git a/src/state/feed-feedback.tsx b/src/state/feed-feedback.tsx
index 8b235f492..3e9c2bafa 100644
--- a/src/state/feed-feedback.tsx
+++ b/src/state/feed-feedback.tsx
@@ -10,17 +10,58 @@ import {AppState, type AppStateStatus} from 'react-native'
 import {type AppBskyFeedDefs} from '@atproto/api'
 import throttle from 'lodash.throttle'
 
-import {FEEDBACK_FEEDS, STAGING_FEEDS} from '#/lib/constants'
+import {PROD_FEEDS, STAGING_FEEDS} from '#/lib/constants'
 import {isNetworkError} from '#/lib/hooks/useCleanError'
 import {logEvent} from '#/lib/statsig/statsig'
 import {Logger} from '#/logger'
 import {
+  type FeedSourceFeedInfo,
+  type FeedSourceInfo,
+  isFeedSourceFeedInfo,
+} from '#/state/queries/feed'
+import {
   type FeedDescriptor,
   type FeedPostSliceItem,
 } from '#/state/queries/post-feed'
 import {getItemsForFeedback} from '#/view/com/posts/PostFeed'
 import {useAgent} from './session'
 
+export const FEEDBACK_FEEDS = [...PROD_FEEDS, ...STAGING_FEEDS]
+
+export const PASSIVE_FEEDBACK_INTERACTIONS = [
+  'app.bsky.feed.defs#clickthroughItem',
+  'app.bsky.feed.defs#clickthroughAuthor',
+  'app.bsky.feed.defs#clickthroughReposter',
+  'app.bsky.feed.defs#clickthroughEmbed',
+  'app.bsky.feed.defs#interactionSeen',
+] as const
+
+export type PassiveFeedbackInteraction =
+  (typeof PASSIVE_FEEDBACK_INTERACTIONS)[number]
+
+export const DIRECT_FEEDBACK_INTERACTIONS = [
+  'app.bsky.feed.defs#requestLess',
+  'app.bsky.feed.defs#requestMore',
+] as const
+
+export type DirectFeedbackInteraction =
+  (typeof DIRECT_FEEDBACK_INTERACTIONS)[number]
+
+export const ALL_FEEDBACK_INTERACTIONS = [
+  ...PASSIVE_FEEDBACK_INTERACTIONS,
+  ...DIRECT_FEEDBACK_INTERACTIONS,
+] as const
+
+export type FeedbackInteraction = (typeof ALL_FEEDBACK_INTERACTIONS)[number]
+
+export function isFeedbackInteraction(
+  interactionEvent: string,
+): interactionEvent is FeedbackInteraction {
+  return ALL_FEEDBACK_INTERACTIONS.includes(
+    interactionEvent as FeedbackInteraction,
+  )
+}
+
 const logger = Logger.create(Logger.Context.FeedFeedback)
 
 export type StateContext = {
@@ -28,6 +69,7 @@ export type StateContext = {
   onItemSeen: (item: any) => void
   sendInteraction: (interaction: AppBskyFeedDefs.Interaction) => void
   feedDescriptor: FeedDescriptor | undefined
+  feedSourceInfo: FeedSourceInfo | undefined
 }
 
 const stateContext = createContext<StateContext>({
@@ -35,15 +77,27 @@ const stateContext = createContext<StateContext>({
   onItemSeen: (_item: any) => {},
   sendInteraction: (_interaction: AppBskyFeedDefs.Interaction) => {},
   feedDescriptor: undefined,
+  feedSourceInfo: undefined,
 })
 stateContext.displayName = 'FeedFeedbackContext'
 
 export function useFeedFeedback(
-  feed: FeedDescriptor | undefined,
+  feedSourceInfo: FeedSourceInfo | undefined,
   hasSession: boolean,
 ) {
   const agent = useAgent()
-  const enabled = isDiscoverFeed(feed) && hasSession
+
+  const feed =
+    !!feedSourceInfo && isFeedSourceFeedInfo(feedSourceInfo)
+      ? feedSourceInfo
+      : undefined
+
+  const isDiscover = isDiscoverFeed(feed?.feedDescriptor)
+  const acceptsInteractions = Boolean(isDiscover || feed?.acceptsInteractions)
+  const proxyDid = feed?.view?.did
+  const enabled =
+    Boolean(feed) && Boolean(proxyDid) && acceptsInteractions && hasSession
+  const enabledInteractions = getEnabledInteractions(enabled, feed, isDiscover)
 
   const queue = useRef<Set<string>>(new Set())
   const history = useRef<
@@ -66,19 +120,24 @@ export function useFeedFeedback(
     const interactions = Array.from(queue.current).map(toInteraction)
     queue.current.clear()
 
-    let proxyDid = 'did:web:discover.bsky.app'
-    if (STAGING_FEEDS.includes(feed ?? '')) {
-      proxyDid = 'did:web:algo.pop2.bsky.app'
+    const interactionsToSend = interactions.filter(
+      interaction =>
+        interaction.event &&
+        isFeedbackInteraction(interaction.event) &&
+        enabledInteractions.includes(interaction.event),
+    )
+
+    if (interactionsToSend.length === 0) {
+      return
     }
 
     // Send to the feed
     agent.app.bsky.feed
       .sendInteractions(
-        {interactions},
+        {interactions: interactionsToSend},
         {
           encoding: 'application/json',
           headers: {
-            // TODO when we start sending to other feeds, we need to grab their DID -prf
             'atproto-proxy': `${proxyDid}#bsky_fg`,
           },
         },
@@ -93,10 +152,13 @@ export function useFeedFeedback(
     if (aggregatedStats.current === null) {
       aggregatedStats.current = createAggregatedStats()
     }
-    sendOrAggregateInteractionsForStats(aggregatedStats.current, interactions)
+    sendOrAggregateInteractionsForStats(
+      aggregatedStats.current,
+      interactionsToSend,
+    )
     throttledFlushAggregatedStats()
     logger.debug('flushed')
-  }, [agent, throttledFlushAggregatedStats, feed])
+  }, [agent, throttledFlushAggregatedStats, proxyDid, enabledInteractions])
 
   const sendToFeed = useMemo(
     () =>
@@ -168,7 +230,8 @@ export function useFeedFeedback(
       // call on various events
       // queues the event to be sent with the throttled sendToFeed call
       sendInteraction,
-      feedDescriptor: feed,
+      feedDescriptor: feed?.feedDescriptor,
+      feedSourceInfo: typeof feed === 'object' ? feed : undefined,
     }
   }, [enabled, onItemSeen, sendInteraction, feed])
 }
@@ -184,10 +247,21 @@ export function useFeedFeedbackContext() {
 // take advantage of the feed feedback API. Until that's in
 // place, we're hardcoding it to the discover feed.
 // -prf
-function isDiscoverFeed(feed?: FeedDescriptor) {
+export function isDiscoverFeed(feed?: FeedDescriptor) {
   return !!feed && FEEDBACK_FEEDS.includes(feed)
 }
 
+function getEnabledInteractions(
+  enabled: boolean,
+  feed: FeedSourceFeedInfo | undefined,
+  isDiscover: boolean,
+): readonly FeedbackInteraction[] {
+  if (!enabled || !feed) {
+    return []
+  }
+  return isDiscover ? ALL_FEEDBACK_INTERACTIONS : DIRECT_FEEDBACK_INTERACTIONS
+}
+
 function toString(interaction: AppBskyFeedDefs.Interaction): string {
   return `${interaction.item}|${interaction.event}|${
     interaction.feedContext || ''
diff --git a/src/state/messages/convo/agent.ts b/src/state/messages/convo/agent.ts
index 2ad4c592e..168002b1f 100644
--- a/src/state/messages/convo/agent.ts
+++ b/src/state/messages/convo/agent.ts
@@ -10,6 +10,7 @@ import EventEmitter from 'eventemitter3'
 import {nanoid} from 'nanoid/non-secure'
 
 import {networkRetry} from '#/lib/async/retry'
+import {DM_SERVICE_HEADERS} from '#/lib/constants'
 import {isNetworkError} from '#/lib/strings/errors'
 import {Logger} from '#/logger'
 import {isNative} from '#/platform/detection'
@@ -33,7 +34,6 @@ import {
 } from '#/state/messages/convo/types'
 import {type MessagesEventBus} from '#/state/messages/events/agent'
 import {type MessagesEventBusError} from '#/state/messages/events/types'
-import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 
 const logger = Logger.create(Logger.Context.ConversationAgent)
 
diff --git a/src/state/messages/events/agent.ts b/src/state/messages/events/agent.ts
index fb3047bf6..e54ea1c77 100644
--- a/src/state/messages/events/agent.ts
+++ b/src/state/messages/events/agent.ts
@@ -3,6 +3,7 @@ import EventEmitter from 'eventemitter3'
 import {nanoid} from 'nanoid/non-secure'
 
 import {networkRetry} from '#/lib/async/retry'
+import {DM_SERVICE_HEADERS} from '#/lib/constants'
 import {isNetworkError} from '#/lib/strings/errors'
 import {Logger} from '#/logger'
 import {
@@ -17,7 +18,6 @@ import {
   type MessagesEventBusParams,
   MessagesEventBusStatus,
 } from '#/state/messages/events/types'
-import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 
 const logger = Logger.create(Logger.Context.DMsAgent)
 
diff --git a/src/state/modals/index.tsx b/src/state/modals/index.tsx
index 9197a66c9..c6070e97b 100644
--- a/src/state/modals/index.tsx
+++ b/src/state/modals/index.tsx
@@ -35,25 +35,15 @@ export interface ContentLanguagesSettingsModal {
   name: 'content-languages-settings'
 }
 
-export interface PostLanguagesSettingsModal {
-  name: 'post-languages-settings'
-}
-
-export interface ChangePasswordModal {
-  name: 'change-password'
-}
-
 /**
  * @deprecated DO NOT ADD NEW MODALS
  */
 export type Modal =
   // Account
   | DeleteAccountModal
-  | ChangePasswordModal
 
   // Curation
   | ContentLanguagesSettingsModal
-  | PostLanguagesSettingsModal
 
   // Lists
   | CreateOrEditListModal
diff --git a/src/state/queries/actor-starter-packs.ts b/src/state/queries/actor-starter-packs.ts
index 670544dfe..d40e05453 100644
--- a/src/state/queries/actor-starter-packs.ts
+++ b/src/state/queries/actor-starter-packs.ts
@@ -1,15 +1,23 @@
-import {AppBskyGraphGetActorStarterPacks} from '@atproto/api'
 import {
-  InfiniteData,
-  QueryClient,
-  QueryKey,
+  type AppBskyGraphGetActorStarterPacks,
+  type AppBskyGraphGetStarterPacksWithMembership,
+} from '@atproto/api'
+import {
+  type InfiniteData,
+  type QueryClient,
+  type QueryKey,
   useInfiniteQuery,
 } from '@tanstack/react-query'
 
 import {useAgent} from '#/state/session'
 
 export const RQKEY_ROOT = 'actor-starter-packs'
+export const RQKEY_WITH_MEMBERSHIP_ROOT = 'actor-starter-packs-with-membership'
 export const RQKEY = (did?: string) => [RQKEY_ROOT, did]
+export const RQKEY_WITH_MEMBERSHIP = (did?: string) => [
+  RQKEY_WITH_MEMBERSHIP_ROOT,
+  did,
+]
 
 export function useActorStarterPacksQuery({
   did,
@@ -42,6 +50,37 @@ export function useActorStarterPacksQuery({
   })
 }
 
+export function useActorStarterPacksWithMembershipsQuery({
+  did,
+  enabled = true,
+}: {
+  did?: string
+  enabled?: boolean
+}) {
+  const agent = useAgent()
+
+  return useInfiniteQuery<
+    AppBskyGraphGetStarterPacksWithMembership.OutputSchema,
+    Error,
+    InfiniteData<AppBskyGraphGetStarterPacksWithMembership.OutputSchema>,
+    QueryKey,
+    string | undefined
+  >({
+    queryKey: RQKEY_WITH_MEMBERSHIP(did),
+    queryFn: async ({pageParam}: {pageParam?: string}) => {
+      const res = await agent.app.bsky.graph.getStarterPacksWithMembership({
+        actor: did!,
+        limit: 10,
+        cursor: pageParam,
+      })
+      return res.data
+    },
+    enabled: Boolean(did) && enabled,
+    initialPageParam: undefined,
+    getNextPageParam: lastPage => lastPage.cursor,
+  })
+}
+
 export async function invalidateActorStarterPacksQuery({
   queryClient,
   did,
@@ -51,3 +90,13 @@ export async function invalidateActorStarterPacksQuery({
 }) {
   await queryClient.invalidateQueries({queryKey: RQKEY(did)})
 }
+
+export async function invalidateActorStarterPacksWithMembershipQuery({
+  queryClient,
+  did,
+}: {
+  queryClient: QueryClient
+  did: string
+}) {
+  await queryClient.invalidateQueries({queryKey: RQKEY_WITH_MEMBERSHIP(did)})
+}
diff --git a/src/state/queries/feed.ts b/src/state/queries/feed.ts
index 89023e513..e6e3e82fb 100644
--- a/src/state/queries/feed.ts
+++ b/src/state/queries/feed.ts
@@ -48,6 +48,7 @@ export type FeedSourceFeedInfo = {
   creatorDid: string
   creatorHandle: string
   likeCount: number | undefined
+  acceptsInteractions?: boolean
   likeUri: string | undefined
   contentMode: AppBskyFeedDefs.GeneratorView['contentMode']
 }
@@ -73,6 +74,12 @@ export type FeedSourceListInfo = {
 
 export type FeedSourceInfo = FeedSourceFeedInfo | FeedSourceListInfo
 
+export function isFeedSourceFeedInfo(
+  feed: FeedSourceInfo,
+): feed is FeedSourceFeedInfo {
+  return feed.type === 'feed'
+}
+
 const feedSourceInfoQueryKeyRoot = 'getFeedSourceInfo'
 export const feedSourceInfoQueryKey = ({uri}: {uri: string}) => [
   feedSourceInfoQueryKeyRoot,
@@ -115,6 +122,7 @@ export function hydrateFeedGenerator(
     creatorDid: view.creator.did,
     creatorHandle: view.creator.handle,
     likeCount: view.likeCount,
+    acceptsInteractions: view.acceptsInteractions,
     likeUri: view.viewer?.like,
     contentMode: view.contentMode,
   }
@@ -619,6 +627,29 @@ export function useSavedFeeds() {
   })
 }
 
+const feedInfoQueryKeyRoot = 'feedInfo'
+
+export function useFeedInfo(feedUri: string | undefined) {
+  const agent = useAgent()
+
+  return useQuery({
+    staleTime: STALE.INFINITY,
+    queryKey: [feedInfoQueryKeyRoot, feedUri],
+    queryFn: async () => {
+      if (!feedUri) {
+        return undefined
+      }
+
+      const res = await agent.app.bsky.feed.getFeedGenerator({
+        feed: feedUri,
+      })
+
+      const feedSourceInfo = hydrateFeedGenerator(res.data.view)
+      return feedSourceInfo
+    },
+  })
+}
+
 function precacheFeed(queryClient: QueryClient, hydratedFeed: FeedSourceInfo) {
   precacheResolvedUri(
     queryClient,
diff --git a/src/state/queries/handle-availability.ts b/src/state/queries/handle-availability.ts
index 9391f5d09..06fc6eebb 100644
--- a/src/state/queries/handle-availability.ts
+++ b/src/state/queries/handle-availability.ts
@@ -1,4 +1,4 @@
-import {Agent, ComAtprotoTempCheckHandleAvailability} from '@atproto/api'
+import {ComAtprotoTempCheckHandleAvailability} from '@atproto/api'
 import {useQuery} from '@tanstack/react-query'
 
 import {
@@ -10,6 +10,7 @@ import {createFullHandle} from '#/lib/strings/handles'
 import {logger} from '#/logger'
 import {useDebouncedValue} from '#/components/live/utils'
 import * as bsky from '#/types/bsky'
+import {Agent} from '../session/agent'
 
 export const RQKEY_handleAvailability = (
   handle: string,
@@ -74,7 +75,7 @@ export async function checkHandleAvailability(
   },
 ) {
   if (serviceDid === BSKY_SERVICE_DID) {
-    const agent = new Agent({service: BSKY_SERVICE})
+    const agent = new Agent(null, {service: BSKY_SERVICE})
     // entryway has a special API for handle availability
     const {data} = await agent.com.atproto.temp.checkHandleAvailability({
       handle,
@@ -109,7 +110,7 @@ export async function checkHandleAvailability(
     }
   } else {
     // 3rd party PDSes won't have this API so just try and resolve the handle
-    const agent = new Agent({service: PUBLIC_BSKY_SERVICE})
+    const agent = new Agent(null, {service: PUBLIC_BSKY_SERVICE})
     try {
       const res = await agent.resolveHandle({
         handle,
diff --git a/src/state/queries/messages/accept-conversation.ts b/src/state/queries/messages/accept-conversation.ts
index 82acb33c8..0c06055b5 100644
--- a/src/state/queries/messages/accept-conversation.ts
+++ b/src/state/queries/messages/accept-conversation.ts
@@ -1,9 +1,12 @@
-import {ChatBskyConvoAcceptConvo, ChatBskyConvoListConvos} from '@atproto/api'
+import {
+  type ChatBskyConvoAcceptConvo,
+  type ChatBskyConvoListConvos,
+} from '@atproto/api'
 import {useMutation, useQueryClient} from '@tanstack/react-query'
 
+import {DM_SERVICE_HEADERS} from '#/lib/constants'
 import {logger} from '#/logger'
 import {useAgent} from '#/state/session'
-import {DM_SERVICE_HEADERS} from './const'
 import {
   RQKEY as CONVO_LIST_KEY,
   RQKEY_ROOT as CONVO_LIST_ROOT_KEY,
diff --git a/src/state/queries/messages/const.ts b/src/state/queries/messages/const.ts
deleted file mode 100644
index 1c5519a63..000000000
--- a/src/state/queries/messages/const.ts
+++ /dev/null
@@ -1,5 +0,0 @@
-import {CHAT_PROXY_DID} from '#/env'
-
-export const DM_SERVICE_HEADERS = {
-  'atproto-proxy': `${CHAT_PROXY_DID}#bsky_chat`,
-}
diff --git a/src/state/queries/messages/conversation.ts b/src/state/queries/messages/conversation.ts
index de5a90571..393bf9e52 100644
--- a/src/state/queries/messages/conversation.ts
+++ b/src/state/queries/messages/conversation.ts
@@ -1,17 +1,17 @@
-import {ChatBskyConvoDefs} from '@atproto/api'
+import {type ChatBskyConvoDefs} from '@atproto/api'
 import {
-  QueryClient,
+  type QueryClient,
   useMutation,
   useQuery,
   useQueryClient,
 } from '@tanstack/react-query'
 
+import {DM_SERVICE_HEADERS} from '#/lib/constants'
 import {STALE} from '#/state/queries'
-import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 import {useOnMarkAsRead} from '#/state/queries/messages/list-conversations'
 import {useAgent} from '#/state/session'
 import {
-  ConvoListQueryData,
+  type ConvoListQueryData,
   getConvoFromQueryData,
   RQKEY_ROOT as LIST_CONVOS_KEY,
 } from './list-conversations'
diff --git a/src/state/queries/messages/get-convo-availability.ts b/src/state/queries/messages/get-convo-availability.ts
index f545c3bba..2392edb09 100644
--- a/src/state/queries/messages/get-convo-availability.ts
+++ b/src/state/queries/messages/get-convo-availability.ts
@@ -1,6 +1,6 @@
 import {useQuery} from '@tanstack/react-query'
 
-import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
+import {DM_SERVICE_HEADERS} from '#/lib/constants'
 import {useAgent} from '#/state/session'
 import {STALE} from '..'
 
diff --git a/src/state/queries/messages/get-convo-for-members.ts b/src/state/queries/messages/get-convo-for-members.ts
index 3f45c2328..58c1ab524 100644
--- a/src/state/queries/messages/get-convo-for-members.ts
+++ b/src/state/queries/messages/get-convo-for-members.ts
@@ -1,8 +1,8 @@
-import {ChatBskyConvoGetConvoForMembers} from '@atproto/api'
+import {type ChatBskyConvoGetConvoForMembers} from '@atproto/api'
 import {useMutation, useQueryClient} from '@tanstack/react-query'
 
+import {DM_SERVICE_HEADERS} from '#/lib/constants'
 import {logger} from '#/logger'
-import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 import {useAgent} from '#/state/session'
 import {precacheConvoQuery} from './conversation'
 
diff --git a/src/state/queries/messages/leave-conversation.ts b/src/state/queries/messages/leave-conversation.ts
index b17e515be..986351a07 100644
--- a/src/state/queries/messages/leave-conversation.ts
+++ b/src/state/queries/messages/leave-conversation.ts
@@ -1,13 +1,16 @@
 import {useMemo} from 'react'
-import {ChatBskyConvoLeaveConvo, ChatBskyConvoListConvos} from '@atproto/api'
+import {
+  type ChatBskyConvoLeaveConvo,
+  type ChatBskyConvoListConvos,
+} from '@atproto/api'
 import {
   useMutation,
   useMutationState,
   useQueryClient,
 } from '@tanstack/react-query'
 
+import {DM_SERVICE_HEADERS} from '#/lib/constants'
 import {logger} from '#/logger'
-import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 import {useAgent} from '#/state/session'
 import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
 
diff --git a/src/state/queries/messages/list-conversations.tsx b/src/state/queries/messages/list-conversations.tsx
index 3f8252519..c5457d1cb 100644
--- a/src/state/queries/messages/list-conversations.tsx
+++ b/src/state/queries/messages/list-conversations.tsx
@@ -13,10 +13,10 @@ import {
 } from '@tanstack/react-query'
 import throttle from 'lodash.throttle'
 
+import {DM_SERVICE_HEADERS} from '#/lib/constants'
 import {useCurrentConvoId} from '#/state/messages/current-convo-id'
 import {useMessagesEventBus} from '#/state/messages/events'
 import {useModerationOpts} from '#/state/preferences/moderation-opts'
-import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 import {useAgent, useSession} from '#/state/session'
 import {useLeftConvos} from './leave-conversation'
 
diff --git a/src/state/queries/messages/mute-conversation.ts b/src/state/queries/messages/mute-conversation.ts
index da9644145..d668e36cb 100644
--- a/src/state/queries/messages/mute-conversation.ts
+++ b/src/state/queries/messages/mute-conversation.ts
@@ -1,11 +1,15 @@
 import {
-  ChatBskyConvoDefs,
-  ChatBskyConvoListConvos,
-  ChatBskyConvoMuteConvo,
+  type ChatBskyConvoDefs,
+  type ChatBskyConvoListConvos,
+  type ChatBskyConvoMuteConvo,
 } from '@atproto/api'
-import {InfiniteData, useMutation, useQueryClient} from '@tanstack/react-query'
+import {
+  type InfiniteData,
+  useMutation,
+  useQueryClient,
+} from '@tanstack/react-query'
 
-import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
+import {DM_SERVICE_HEADERS} from '#/lib/constants'
 import {useAgent} from '#/state/session'
 import {RQKEY as CONVO_KEY} from './conversation'
 import {RQKEY_ROOT as CONVO_LIST_KEY} from './list-conversations'
diff --git a/src/state/queries/messages/update-all-read.ts b/src/state/queries/messages/update-all-read.ts
index 72fa65ee6..3d0fd3a45 100644
--- a/src/state/queries/messages/update-all-read.ts
+++ b/src/state/queries/messages/update-all-read.ts
@@ -1,8 +1,8 @@
-import {ChatBskyConvoListConvos} from '@atproto/api'
+import {type ChatBskyConvoListConvos} from '@atproto/api'
 import {useMutation, useQueryClient} from '@tanstack/react-query'
 
+import {DM_SERVICE_HEADERS} from '#/lib/constants'
 import {logger} from '#/logger'
-import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 import {useAgent} from '#/state/session'
 import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
 
diff --git a/src/state/queries/service.ts b/src/state/queries/service.ts
index 6bfd0b011..e9661db9e 100644
--- a/src/state/queries/service.ts
+++ b/src/state/queries/service.ts
@@ -1,6 +1,7 @@
-import {BskyAgent} from '@atproto/api'
 import {useQuery} from '@tanstack/react-query'
 
+import {Agent} from '../session/agent'
+
 const RQKEY_ROOT = 'service'
 export const RQKEY = (serviceUrl: string) => [RQKEY_ROOT, serviceUrl]
 
@@ -8,7 +9,7 @@ export function useServiceQuery(serviceUrl: string) {
   return useQuery({
     queryKey: RQKEY(serviceUrl),
     queryFn: async () => {
-      const agent = new BskyAgent({service: serviceUrl})
+      const agent = new Agent(null, {service: serviceUrl})
       const res = await agent.com.atproto.server.describeServer()
       return res.data
     },
diff --git a/src/state/queries/trending/useGetSuggestedUsersQuery.ts b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
index 05cc4d74d..898029398 100644
--- a/src/state/queries/trending/useGetSuggestedUsersQuery.ts
+++ b/src/state/queries/trending/useGetSuggestedUsersQuery.ts
@@ -17,6 +17,7 @@ export type QueryProps = {
   category?: string | null
   limit?: number
   enabled?: boolean
+  overrideInterests?: string[]
 }
 
 export const getSuggestedUsersQueryKeyRoot = 'unspecced-suggested-users'
@@ -24,6 +25,7 @@ export const createGetSuggestedUsersQueryKey = (props: QueryProps) => [
   getSuggestedUsersQueryKeyRoot,
   props.category,
   props.limit,
+  props.overrideInterests?.join(','),
 ]
 
 export function useGetSuggestedUsersQuery(props: QueryProps) {
@@ -36,6 +38,7 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
     queryKey: createGetSuggestedUsersQueryKey(props),
     queryFn: async () => {
       const contentLangs = getContentLanguages().join(',')
+      const interests = aggregateUserInterests(preferences)
       const {data} = await agent.app.bsky.unspecced.getSuggestedUsers(
         {
           category: props.category ?? undefined,
@@ -43,7 +46,11 @@ export function useGetSuggestedUsersQuery(props: QueryProps) {
         },
         {
           headers: {
-            ...createBskyTopicsHeader(aggregateUserInterests(preferences)),
+            ...createBskyTopicsHeader(
+              props.overrideInterests && props.overrideInterests.length > 0
+                ? props.overrideInterests.join(',')
+                : interests,
+            ),
             'Accept-Language': contentLangs,
           },
         },
diff --git a/src/state/session/agent.ts b/src/state/session/agent.ts
index 531e285ab..d063a09a2 100644
--- a/src/state/session/agent.ts
+++ b/src/state/session/agent.ts
@@ -1,8 +1,19 @@
-import {AtpSessionData, AtpSessionEvent, BskyAgent} from '@atproto/api'
+import {
+  Agent as BaseAgent,
+  type AtprotoServiceType,
+  type AtpSessionData,
+  type AtpSessionEvent,
+  BskyAgent,
+  type Did,
+} from '@atproto/api'
+import {type FetchHandler} from '@atproto/api/dist/agent'
+import {type SessionManager} from '@atproto/api/dist/session-manager'
 import {TID} from '@atproto/common-web'
+import {type FetchHandlerOptions} from '@atproto/xrpc'
 
 import {networkRetry} from '#/lib/async/retry'
 import {
+  BLUESKY_PROXY_HEADER,
   BSKY_SERVICE,
   DISCOVER_SAVED_FEED,
   IS_PROD_SERVICE,
@@ -19,12 +30,17 @@ import {
   configureModerationForAccount,
   configureModerationForGuest,
 } from './moderation'
-import {SessionAccount} from './types'
+import {type SessionAccount} from './types'
 import {isSessionExpired, isSignupQueued} from './util'
 
+export type ProxyHeaderValue = `${Did}#${AtprotoServiceType}`
+
 export function createPublicAgent() {
   configureModerationForGuest() // Side effect but only relevant for tests
-  return new BskyAppAgent({service: PUBLIC_BSKY_SERVICE})
+
+  const agent = new BskyAppAgent({service: PUBLIC_BSKY_SERVICE})
+  agent.configureProxy(BLUESKY_PROXY_HEADER)
+  return agent
 }
 
 export async function createAgentAndResume(
@@ -61,6 +77,8 @@ export async function createAgentAndResume(
     }
   }
 
+  agent.configureProxy(BLUESKY_PROXY_HEADER)
+
   return agent.prepare(gates, moderation, onSessionChange)
 }
 
@@ -93,6 +111,9 @@ export async function createAgentAndLogin(
   const account = agentToSessionAccountOrThrow(agent)
   const gates = tryFetchGates(account.did, 'prefer-fresh-gates')
   const moderation = configureModerationForAccount(agent, account)
+
+  agent.configureProxy(BLUESKY_PROXY_HEADER)
+
   return agent.prepare(gates, moderation, onSessionChange)
 }
 
@@ -180,6 +201,8 @@ export async function createAgentAndCreateAccount(
     logger.error(e, {message: `session: failed snoozeEmailConfirmationPrompt`})
   }
 
+  agent.configureProxy(BLUESKY_PROXY_HEADER)
+
   return agent.prepare(gates, moderation, onSessionChange)
 }
 
@@ -234,7 +257,22 @@ export function sessionAccountToSession(
   }
 }
 
+export class Agent extends BaseAgent {
+  constructor(
+    proxyHeader: ProxyHeaderValue | null,
+    options: SessionManager | FetchHandler | FetchHandlerOptions,
+  ) {
+    super(options)
+    if (proxyHeader) {
+      this.configureProxy(proxyHeader)
+    }
+  }
+}
+
 // Not exported. Use factories above to create it.
+// WARN: In the factories above, we _manually set a proxy header_ for the agent after we do whatever it is we are supposed to do.
+// Ideally, we wouldn't be doing this. However, since there is so much logic that requires making calls to the PDS right now, it
+// feels safer to just let those run as-is and set the header afterward.
 let realFetch = globalThis.fetch
 class BskyAppAgent extends BskyAgent {
   persistSessionHandler: ((event: AtpSessionEvent) => void) | undefined =
diff --git a/src/state/unstable-post-source.tsx b/src/state/unstable-post-source.tsx
index 450f2c120..17fe18840 100644
--- a/src/state/unstable-post-source.tsx
+++ b/src/state/unstable-post-source.tsx
@@ -2,7 +2,7 @@ import {useEffect, useId, useState} from 'react'
 import {type AppBskyFeedDefs, AtUri} from '@atproto/api'
 
 import {Logger} from '#/logger'
-import {type FeedDescriptor} from '#/state/queries/post-feed'
+import {type FeedSourceInfo} from '#/state/queries/feed'
 
 /**
  * Separate logger for better debugging
@@ -11,7 +11,7 @@ const logger = Logger.create(Logger.Context.PostSource)
 
 export type PostSource = {
   post: AppBskyFeedDefs.FeedViewPost
-  feed?: FeedDescriptor
+  feedSourceInfo?: FeedSourceInfo
 }
 
 /**