about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/App.native.tsx37
-rw-r--r--src/App.web.tsx37
-rw-r--r--src/components/ProfileHoverCard/index.web.tsx2
-rw-r--r--src/components/dms/NewChat.tsx2
-rw-r--r--src/lib/functions.ts87
-rw-r--r--src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx8
-rw-r--r--src/screens/Onboarding/StepSuggestedAccounts/index.tsx2
-rw-r--r--src/state/preferences/moderation-opts.tsx61
-rw-r--r--src/state/queries/actor-autocomplete.ts3
-rw-r--r--src/state/queries/notifications/feed.ts2
-rw-r--r--src/state/queries/notifications/unread.tsx2
-rw-r--r--src/state/queries/post-feed.ts3
-rw-r--r--src/state/queries/preferences/index.ts48
-rw-r--r--src/state/queries/suggested-follows.ts6
-rw-r--r--src/view/com/notifications/Feed.tsx27
-rw-r--r--src/view/com/post-thread/PostThread.tsx6
-rw-r--r--src/view/com/post-thread/PostThreadItem.tsx2
-rw-r--r--src/view/com/post/Post.tsx2
-rw-r--r--src/view/com/profile/ProfileCard.tsx2
-rw-r--r--src/view/com/profile/ProfileHeaderSuggestedFollows.tsx2
-rw-r--r--src/view/com/util/post-embeds/QuoteEmbed.tsx2
-rw-r--r--src/view/screens/DebugMod.tsx2
-rw-r--r--src/view/screens/Profile.tsx2
-rw-r--r--src/view/screens/Search/Search.tsx2
-rw-r--r--src/view/shell/desktop/Search.tsx2
25 files changed, 231 insertions, 120 deletions
diff --git a/src/App.native.tsx b/src/App.native.tsx
index 569f342c2..14daa3bd6 100644
--- a/src/App.native.tsx
+++ b/src/App.native.tsx
@@ -18,6 +18,7 @@ import {useQueryClient} from '@tanstack/react-query'
 import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
 import {init as initPersistedState} from '#/state/persisted'
 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
+import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
 import {readLastActiveAccount} from '#/state/session/util'
 import {useIntentHandler} from 'lib/hooks/useIntentHandler'
 import {useNotificationsListener} from 'lib/notifications/notifications'
@@ -79,23 +80,25 @@ function InnerApp() {
             <QueryProvider currentDid={currentAccount?.did}>
               <PushNotificationsListener>
                 <StatsigProvider>
-                  <LabelDefsProvider>
-                    <LoggedOutViewProvider>
-                      <SelectedFeedProvider>
-                        <UnreadNotifsProvider>
-                          <ThemeProvider theme={theme}>
-                            {/* All components should be within this provider */}
-                            <RootSiblingParent>
-                              <GestureHandlerRootView style={s.h100pct}>
-                                <TestCtrls />
-                                <Shell />
-                              </GestureHandlerRootView>
-                            </RootSiblingParent>
-                          </ThemeProvider>
-                        </UnreadNotifsProvider>
-                      </SelectedFeedProvider>
-                    </LoggedOutViewProvider>
-                  </LabelDefsProvider>
+                  <ModerationOptsProvider>
+                    <LabelDefsProvider>
+                      <LoggedOutViewProvider>
+                        <SelectedFeedProvider>
+                          <UnreadNotifsProvider>
+                            <ThemeProvider theme={theme}>
+                              {/* All components should be within this provider */}
+                              <RootSiblingParent>
+                                <GestureHandlerRootView style={s.h100pct}>
+                                  <TestCtrls />
+                                  <Shell />
+                                </GestureHandlerRootView>
+                              </RootSiblingParent>
+                            </ThemeProvider>
+                          </UnreadNotifsProvider>
+                        </SelectedFeedProvider>
+                      </LoggedOutViewProvider>
+                    </LabelDefsProvider>
+                  </ModerationOptsProvider>
                 </StatsigProvider>
               </PushNotificationsListener>
             </QueryProvider>
diff --git a/src/App.web.tsx b/src/App.web.tsx
index 226fb8d3f..e04fd45f1 100644
--- a/src/App.web.tsx
+++ b/src/App.web.tsx
@@ -8,6 +8,7 @@ import {SafeAreaProvider} from 'react-native-safe-area-context'
 import {Provider as StatsigProvider} from '#/lib/statsig/statsig'
 import {init as initPersistedState} from '#/state/persisted'
 import {Provider as LabelDefsProvider} from '#/state/preferences/label-defs'
+import {Provider as ModerationOptsProvider} from '#/state/preferences/moderation-opts'
 import {readLastActiveAccount} from '#/state/session/util'
 import {useIntentHandler} from 'lib/hooks/useIntentHandler'
 import {QueryProvider} from 'lib/react-query'
@@ -56,23 +57,25 @@ function InnerApp() {
         key={currentAccount?.did}>
         <QueryProvider currentDid={currentAccount?.did}>
           <StatsigProvider>
-            <LabelDefsProvider>
-              <LoggedOutViewProvider>
-                <SelectedFeedProvider>
-                  <UnreadNotifsProvider>
-                    <ThemeProvider theme={theme}>
-                      {/* All components should be within this provider */}
-                      <RootSiblingParent>
-                        <SafeAreaProvider>
-                          <Shell />
-                        </SafeAreaProvider>
-                      </RootSiblingParent>
-                      <ToastContainer />
-                    </ThemeProvider>
-                  </UnreadNotifsProvider>
-                </SelectedFeedProvider>
-              </LoggedOutViewProvider>
-            </LabelDefsProvider>
+            <ModerationOptsProvider>
+              <LabelDefsProvider>
+                <LoggedOutViewProvider>
+                  <SelectedFeedProvider>
+                    <UnreadNotifsProvider>
+                      <ThemeProvider theme={theme}>
+                        {/* All components should be within this provider */}
+                        <RootSiblingParent>
+                          <SafeAreaProvider>
+                            <Shell />
+                          </SafeAreaProvider>
+                        </RootSiblingParent>
+                        <ToastContainer />
+                      </ThemeProvider>
+                    </UnreadNotifsProvider>
+                  </SelectedFeedProvider>
+                </LoggedOutViewProvider>
+              </LabelDefsProvider>
+            </ModerationOptsProvider>
           </StatsigProvider>
         </QueryProvider>
       </React.Fragment>
diff --git a/src/components/ProfileHoverCard/index.web.tsx b/src/components/ProfileHoverCard/index.web.tsx
index d1b00d522..a22436879 100644
--- a/src/components/ProfileHoverCard/index.web.tsx
+++ b/src/components/ProfileHoverCard/index.web.tsx
@@ -9,7 +9,7 @@ import {makeProfileLink} from '#/lib/routes/links'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {pluralize} from '#/lib/strings/helpers'
-import {useModerationOpts} from '#/state/queries/preferences'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {usePrefetchProfileQuery, useProfileQuery} from '#/state/queries/profile'
 import {useSession} from '#/state/session'
 import {useProfileShadow} from 'state/cache/profile-shadow'
diff --git a/src/components/dms/NewChat.tsx b/src/components/dms/NewChat.tsx
index 86d421eba..3ad1ce7ee 100644
--- a/src/components/dms/NewChat.tsx
+++ b/src/components/dms/NewChat.tsx
@@ -8,7 +8,7 @@ import {useLingui} from '@lingui/react'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {isWeb} from '#/platform/detection'
-import {useModerationOpts} from '#/state/queries/preferences'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useActorAutocompleteQuery} from 'state/queries/actor-autocomplete'
 import {FAB} from '#/view/com/util/fab/FAB'
 import * as Toast from '#/view/com/util/Toast'
diff --git a/src/lib/functions.ts b/src/lib/functions.ts
index b45c7fa6d..e0d44ce2d 100644
--- a/src/lib/functions.ts
+++ b/src/lib/functions.ts
@@ -9,3 +9,90 @@ export function dedupArray<T>(arr: T[]): T[] {
   const s = new Set(arr)
   return [...s]
 }
+
+/**
+ * Taken from @tanstack/query-core utils.ts
+ * Modified to support Date object comparisons
+ *
+ * This function returns `a` if `b` is deeply equal.
+ * If not, it will replace any deeply equal children of `b` with those of `a`.
+ * This can be used for structural sharing between JSON values for example.
+ */
+export function replaceEqualDeep(a: any, b: any): any {
+  if (a === b) {
+    return a
+  }
+
+  if (a instanceof Date && b instanceof Date) {
+    return a.getTime() === b.getTime() ? a : b
+  }
+
+  const array = isPlainArray(a) && isPlainArray(b)
+
+  if (array || (isPlainObject(a) && isPlainObject(b))) {
+    const aItems = array ? a : Object.keys(a)
+    const aSize = aItems.length
+    const bItems = array ? b : Object.keys(b)
+    const bSize = bItems.length
+    const copy: any = array ? [] : {}
+
+    let equalItems = 0
+
+    for (let i = 0; i < bSize; i++) {
+      const key = array ? i : bItems[i]
+      if (
+        !array &&
+        a[key] === undefined &&
+        b[key] === undefined &&
+        aItems.includes(key)
+      ) {
+        copy[key] = undefined
+        equalItems++
+      } else {
+        copy[key] = replaceEqualDeep(a[key], b[key])
+        if (copy[key] === a[key] && a[key] !== undefined) {
+          equalItems++
+        }
+      }
+    }
+
+    return aSize === bSize && equalItems === aSize ? a : copy
+  }
+
+  return b
+}
+
+export function isPlainArray(value: unknown) {
+  return Array.isArray(value) && value.length === Object.keys(value).length
+}
+
+// Copied from: https://github.com/jonschlinkert/is-plain-object
+export function isPlainObject(o: any): o is Object {
+  if (!hasObjectPrototype(o)) {
+    return false
+  }
+
+  // If has no constructor
+  const ctor = o.constructor
+  if (ctor === undefined) {
+    return true
+  }
+
+  // If has modified prototype
+  const prot = ctor.prototype
+  if (!hasObjectPrototype(prot)) {
+    return false
+  }
+
+  // If constructor does not have an Object-specific method
+  if (!prot.hasOwnProperty('isPrototypeOf')) {
+    return false
+  }
+
+  // Most likely a plain Object
+  return true
+}
+
+function hasObjectPrototype(o: any): boolean {
+  return Object.prototype.toString.call(o) === '[object Object]'
+}
diff --git a/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx b/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx
index 7e4ea1f8b..f0ba36e39 100644
--- a/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx
+++ b/src/screens/Onboarding/StepSuggestedAccounts/SuggestedAccountCard.tsx
@@ -2,13 +2,13 @@ import React from 'react'
 import {View, ViewStyle} from 'react-native'
 import {AppBskyActorDefs, moderateProfile} from '@atproto/api'
 
-import {useTheme, atoms as a, flatten} from '#/alf'
-import {Text} from '#/components/Typography'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
+import {UserAvatar} from '#/view/com/util/UserAvatar'
+import {atoms as a, flatten, useTheme} from '#/alf'
 import {useItemContext} from '#/components/forms/Toggle'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
-import {UserAvatar} from '#/view/com/util/UserAvatar'
-import {useModerationOpts} from '#/state/queries/preferences'
 import {RichText} from '#/components/RichText'
+import {Text} from '#/components/Typography'
 
 export function SuggestedAccountCard({
   profile,
diff --git a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx
index e9bc3f0fd..7b2ad2b99 100644
--- a/src/screens/Onboarding/StepSuggestedAccounts/index.tsx
+++ b/src/screens/Onboarding/StepSuggestedAccounts/index.tsx
@@ -7,7 +7,7 @@ import {useLingui} from '@lingui/react'
 import {useAnalytics} from '#/lib/analytics/analytics'
 import {logEvent} from '#/lib/statsig/statsig'
 import {capitalize} from '#/lib/strings/capitalize'
-import {useModerationOpts} from '#/state/queries/preferences'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useProfilesQuery} from '#/state/queries/profile'
 import {
   DescriptionText,
diff --git a/src/state/preferences/moderation-opts.tsx b/src/state/preferences/moderation-opts.tsx
new file mode 100644
index 000000000..b0278d5e8
--- /dev/null
+++ b/src/state/preferences/moderation-opts.tsx
@@ -0,0 +1,61 @@
+import React, {createContext, useContext, useMemo} from 'react'
+import {BSKY_LABELER_DID, ModerationOpts} from '@atproto/api'
+
+import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences'
+import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
+import {useSession} from '#/state/session'
+import {usePreferencesQuery} from '../queries/preferences'
+
+export const moderationOptsContext = createContext<ModerationOpts | undefined>(
+  undefined,
+)
+
+// used in the moderation state devtool
+export const moderationOptsOverrideContext = createContext<
+  ModerationOpts | undefined
+>(undefined)
+
+export function useModerationOpts() {
+  return useContext(moderationOptsContext)
+}
+
+export function Provider({children}: React.PropsWithChildren<{}>) {
+  const override = useContext(moderationOptsOverrideContext)
+  const {currentAccount} = useSession()
+  const prefs = usePreferencesQuery()
+  const {labelDefs} = useLabelDefinitions()
+  const hiddenPosts = useHiddenPosts() // TODO move this into pds-stored prefs
+
+  const userDid = currentAccount?.did
+  const moderationPrefs = prefs.data?.moderationPrefs
+  const value = useMemo<ModerationOpts | undefined>(() => {
+    if (override) {
+      return override
+    }
+    if (!moderationPrefs) {
+      return undefined
+    }
+    return {
+      userDid,
+      prefs: {
+        ...moderationPrefs,
+        labelers: moderationPrefs.labelers.length
+          ? moderationPrefs.labelers
+          : [
+              {
+                did: BSKY_LABELER_DID,
+                labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
+              },
+            ],
+        hiddenPosts: hiddenPosts || [],
+      },
+      labelDefs,
+    }
+  }, [override, userDid, labelDefs, moderationPrefs, hiddenPosts])
+
+  return (
+    <moderationOptsContext.Provider value={value}>
+      {children}
+    </moderationOptsContext.Provider>
+  )
+}
diff --git a/src/state/queries/actor-autocomplete.ts b/src/state/queries/actor-autocomplete.ts
index 98b5aa17e..430f6978a 100644
--- a/src/state/queries/actor-autocomplete.ts
+++ b/src/state/queries/actor-autocomplete.ts
@@ -6,7 +6,8 @@ import {isJustAMute} from '#/lib/moderation'
 import {logger} from '#/logger'
 import {STALE} from '#/state/queries'
 import {useAgent} from '#/state/session'
-import {DEFAULT_LOGGED_OUT_PREFERENCES, useModerationOpts} from './preferences'
+import {useModerationOpts} from '../preferences/moderation-opts'
+import {DEFAULT_LOGGED_OUT_PREFERENCES} from './preferences'
 
 const DEFAULT_MOD_OPTS = {
   userDid: undefined,
diff --git a/src/state/queries/notifications/feed.ts b/src/state/queries/notifications/feed.ts
index 1f2199901..80e5a4c47 100644
--- a/src/state/queries/notifications/feed.ts
+++ b/src/state/queries/notifications/feed.ts
@@ -28,8 +28,8 @@ import {
 
 import {useMutedThreads} from '#/state/muted-threads'
 import {useAgent} from '#/state/session'
+import {useModerationOpts} from '../../preferences/moderation-opts'
 import {STALE} from '..'
-import {useModerationOpts} from '../preferences'
 import {embedViewRecordToPostView, getEmbeddedPost} from '../util'
 import {FeedPage} from './types'
 import {useUnreadNotificationsApi} from './unread'
diff --git a/src/state/queries/notifications/unread.tsx b/src/state/queries/notifications/unread.tsx
index 1c569e2a0..80333b524 100644
--- a/src/state/queries/notifications/unread.tsx
+++ b/src/state/queries/notifications/unread.tsx
@@ -13,7 +13,7 @@ import {logger} from '#/logger'
 import {isNative} from '#/platform/detection'
 import {useMutedThreads} from '#/state/muted-threads'
 import {useAgent, useSession} from '#/state/session'
-import {useModerationOpts} from '../preferences'
+import {useModerationOpts} from '../../preferences/moderation-opts'
 import {truncateAndInvalidate} from '../util'
 import {RQKEY as RQKEY_NOTIFS} from './feed'
 import {CachedFeedPage, FeedPage} from './types'
diff --git a/src/state/queries/post-feed.ts b/src/state/queries/post-feed.ts
index c265cecd6..827f8a2a8 100644
--- a/src/state/queries/post-feed.ts
+++ b/src/state/queries/post-feed.ts
@@ -32,7 +32,8 @@ import {FeedTuner, FeedTunerFn, NoopFeedTuner} from 'lib/api/feed-manip'
 import {BSKY_FEED_OWNER_DIDS} from 'lib/constants'
 import {KnownError} from '#/view/com/posts/FeedErrorMessage'
 import {useFeedTuners} from '../preferences/feed-tuners'
-import {useModerationOpts, usePreferencesQuery} from './preferences'
+import {useModerationOpts} from '../preferences/moderation-opts'
+import {usePreferencesQuery} from './preferences'
 import {embedViewRecordToPostView, getEmbeddedPost} from './util'
 
 type ActorDid = string
diff --git a/src/state/queries/preferences/index.ts b/src/state/queries/preferences/index.ts
index 06e47391f..f51eaac2a 100644
--- a/src/state/queries/preferences/index.ts
+++ b/src/state/queries/preferences/index.ts
@@ -1,28 +1,24 @@
-import {createContext, useContext, useMemo} from 'react'
 import {
   AppBskyActorDefs,
-  BSKY_LABELER_DID,
   BskyFeedViewPreference,
   LabelPreference,
-  ModerationOpts,
 } from '@atproto/api'
 import {useMutation, useQuery, useQueryClient} from '@tanstack/react-query'
 
 import {track} from '#/lib/analytics/analytics'
+import {replaceEqualDeep} from '#/lib/functions'
 import {getAge} from '#/lib/strings/time'
-import {useHiddenPosts, useLabelDefinitions} from '#/state/preferences'
 import {STALE} from '#/state/queries'
 import {
   DEFAULT_HOME_FEED_PREFS,
   DEFAULT_LOGGED_OUT_PREFERENCES,
   DEFAULT_THREAD_VIEW_PREFS,
 } from '#/state/queries/preferences/const'
-import {DEFAULT_LOGGED_OUT_LABEL_PREFERENCES} from '#/state/queries/preferences/moderation'
 import {
   ThreadViewPreferences,
   UsePreferencesQueryResponse,
 } from '#/state/queries/preferences/types'
-import {useAgent, useSession} from '#/state/session'
+import {useAgent} from '#/state/session'
 import {saveLabelers} from '#/state/session/agent-config'
 
 export * from '#/state/queries/preferences/const'
@@ -36,7 +32,7 @@ export function usePreferencesQuery() {
   const {getAgent} = useAgent()
   return useQuery({
     staleTime: STALE.SECONDS.FIFTEEN,
-    structuralSharing: true,
+    structuralSharing: replaceEqualDeep,
     refetchOnWindowFocus: true,
     queryKey: preferencesQueryKey,
     queryFn: async () => {
@@ -79,44 +75,6 @@ export function usePreferencesQuery() {
   })
 }
 
-// used in the moderation state devtool
-export const moderationOptsOverrideContext = createContext<
-  ModerationOpts | undefined
->(undefined)
-
-export function useModerationOpts() {
-  const override = useContext(moderationOptsOverrideContext)
-  const {currentAccount} = useSession()
-  const prefs = usePreferencesQuery()
-  const {labelDefs} = useLabelDefinitions()
-  const hiddenPosts = useHiddenPosts() // TODO move this into pds-stored prefs
-  const opts = useMemo<ModerationOpts | undefined>(() => {
-    if (override) {
-      return override
-    }
-    if (!prefs.data) {
-      return
-    }
-    return {
-      userDid: currentAccount?.did,
-      prefs: {
-        ...prefs.data.moderationPrefs,
-        labelers: prefs.data.moderationPrefs.labelers.length
-          ? prefs.data.moderationPrefs.labelers
-          : [
-              {
-                did: BSKY_LABELER_DID,
-                labels: DEFAULT_LOGGED_OUT_LABEL_PREFERENCES,
-              },
-            ],
-        hiddenPosts: hiddenPosts || [],
-      },
-      labelDefs,
-    }
-  }, [override, currentAccount, labelDefs, prefs.data, hiddenPosts])
-  return opts
-}
-
 export function useClearPreferencesMutation() {
   const queryClient = useQueryClient()
   const {getAgent} = useAgent()
diff --git a/src/state/queries/suggested-follows.ts b/src/state/queries/suggested-follows.ts
index 3338130f4..7740b1977 100644
--- a/src/state/queries/suggested-follows.ts
+++ b/src/state/queries/suggested-follows.ts
@@ -18,11 +18,9 @@ import {
 } from '#/lib/api/feed/utils'
 import {getContentLanguages} from '#/state/preferences/languages'
 import {STALE} from '#/state/queries'
-import {
-  useModerationOpts,
-  usePreferencesQuery,
-} from '#/state/queries/preferences'
+import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useAgent, useSession} from '#/state/session'
+import {useModerationOpts} from '../preferences/moderation-opts'
 
 const suggestedFollowsQueryKeyRoot = 'suggested-follows'
 const suggestedFollowsQueryKey = [suggestedFollowsQueryKeyRoot]
diff --git a/src/view/com/notifications/Feed.tsx b/src/view/com/notifications/Feed.tsx
index dd439d475..7d34596d9 100644
--- a/src/view/com/notifications/Feed.tsx
+++ b/src/view/com/notifications/Feed.tsx
@@ -1,21 +1,22 @@
 import React from 'react'
-import {CenteredView} from '../util/Views'
 import {ActivityIndicator, StyleSheet, View} from 'react-native'
-import {FeedItem} from './FeedItem'
-import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
-import {ErrorMessage} from '../util/error/ErrorMessage'
-import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
-import {EmptyState} from '../util/EmptyState'
-import {s} from 'lib/styles'
+import {msg} from '@lingui/macro'
+import {useLingui} from '@lingui/react'
+
+import {usePalette} from '#/lib/hooks/usePalette'
+import {cleanError} from '#/lib/strings/errors'
+import {logger} from '#/logger'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useNotificationFeedQuery} from '#/state/queries/notifications/feed'
 import {useUnreadNotificationsApi} from '#/state/queries/notifications/unread'
-import {logger} from '#/logger'
-import {cleanError} from '#/lib/strings/errors'
-import {useModerationOpts} from '#/state/queries/preferences'
+import {s} from 'lib/styles'
+import {EmptyState} from '../util/EmptyState'
+import {ErrorMessage} from '../util/error/ErrorMessage'
 import {List, ListRef} from '../util/List'
-import {useLingui} from '@lingui/react'
-import {msg} from '@lingui/macro'
-import {usePalette} from '#/lib/hooks/usePalette'
+import {NotificationFeedLoadingPlaceholder} from '../util/LoadingPlaceholder'
+import {LoadMoreRetryBtn} from '../util/LoadMoreRetryBtn'
+import {CenteredView} from '../util/Views'
+import {FeedItem} from './FeedItem'
 
 const EMPTY_FEED_ITEM = {_reactKey: '__empty__'}
 const LOAD_MORE_ERROR_ITEM = {_reactKey: '__load_more_error__'}
diff --git a/src/view/com/post-thread/PostThread.tsx b/src/view/com/post-thread/PostThread.tsx
index 6e356f994..69b26ad44 100644
--- a/src/view/com/post-thread/PostThread.tsx
+++ b/src/view/com/post-thread/PostThread.tsx
@@ -8,6 +8,7 @@ import {useLingui} from '@lingui/react'
 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
 import {ScrollProvider} from '#/lib/ScrollContext'
 import {isAndroid, isNative, isWeb} from '#/platform/detection'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {
   sortThread,
   ThreadBlocked,
@@ -16,10 +17,7 @@ import {
   ThreadPost,
   usePostThreadQuery,
 } from '#/state/queries/post-thread'
-import {
-  useModerationOpts,
-  usePreferencesQuery,
-} from '#/state/queries/preferences'
+import {usePreferencesQuery} from '#/state/queries/preferences'
 import {useSession} from '#/state/session'
 import {useInitialNumToRender} from 'lib/hooks/useInitialNumToRender'
 import {usePalette} from 'lib/hooks/usePalette'
diff --git a/src/view/com/post-thread/PostThreadItem.tsx b/src/view/com/post-thread/PostThreadItem.tsx
index 564e37e7a..cfb8bd93f 100644
--- a/src/view/com/post-thread/PostThreadItem.tsx
+++ b/src/view/com/post-thread/PostThreadItem.tsx
@@ -15,8 +15,8 @@ import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
 import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
 import {useLanguagePrefs} from '#/state/preferences'
 import {useOpenLink} from '#/state/preferences/in-app-browser'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {ThreadPost} from '#/state/queries/post-thread'
-import {useModerationOpts} from '#/state/queries/preferences'
 import {useComposerControls} from '#/state/shell/composer'
 import {MAX_POST_LINES} from 'lib/constants'
 import {usePalette} from 'lib/hooks/usePalette'
diff --git a/src/view/com/post/Post.tsx b/src/view/com/post/Post.tsx
index 546eb2821..1a7185cd9 100644
--- a/src/view/com/post/Post.tsx
+++ b/src/view/com/post/Post.tsx
@@ -14,7 +14,7 @@ import {useQueryClient} from '@tanstack/react-query'
 
 import {moderatePost_wrapped as moderatePost} from '#/lib/moderatePost_wrapped'
 import {POST_TOMBSTONE, Shadow, usePostShadow} from '#/state/cache/post-shadow'
-import {useModerationOpts} from '#/state/queries/preferences'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useComposerControls} from '#/state/shell/composer'
 import {MAX_POST_LINES} from 'lib/constants'
 import {usePalette} from 'lib/hooks/usePalette'
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index 90ab9b738..6c8978946 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -12,7 +12,7 @@ import {useQueryClient} from '@tanstack/react-query'
 import {useModerationCauseDescription} from '#/lib/moderation/useModerationCauseDescription'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
 import {Shadow} from '#/state/cache/types'
-import {useModerationOpts} from '#/state/queries/preferences'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useSession} from '#/state/session'
 import {usePalette} from 'lib/hooks/usePalette'
 import {getModerationCauseKey, isJustAMute} from 'lib/moderation'
diff --git a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
index 4c9d164f7..bb5ad2a63 100644
--- a/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
+++ b/src/view/com/profile/ProfileHeaderSuggestedFollows.tsx
@@ -9,7 +9,7 @@ import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {useProfileShadow} from '#/state/cache/profile-shadow'
-import {useModerationOpts} from '#/state/queries/preferences'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useProfileFollowMutationQueue} from '#/state/queries/profile'
 import {useSuggestedFollowsByActorQuery} from '#/state/queries/suggested-follows'
 import {useAnalytics} from 'lib/analytics/analytics'
diff --git a/src/view/com/util/post-embeds/QuoteEmbed.tsx b/src/view/com/util/post-embeds/QuoteEmbed.tsx
index e0178f34b..0e19a6ccd 100644
--- a/src/view/com/util/post-embeds/QuoteEmbed.tsx
+++ b/src/view/com/util/post-embeds/QuoteEmbed.tsx
@@ -25,7 +25,7 @@ import {useQueryClient} from '@tanstack/react-query'
 
 import {HITSLOP_20} from '#/lib/constants'
 import {s} from '#/lib/styles'
-import {useModerationOpts} from '#/state/queries/preferences'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {usePalette} from 'lib/hooks/usePalette'
 import {InfoCircleIcon} from 'lib/icons'
 import {makeProfileLink} from 'lib/routes/links'
diff --git a/src/view/screens/DebugMod.tsx b/src/view/screens/DebugMod.tsx
index f88d500f9..442e33fd3 100644
--- a/src/view/screens/DebugMod.tsx
+++ b/src/view/screens/DebugMod.tsx
@@ -20,12 +20,12 @@ import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
 
 import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
+import {moderationOptsOverrideContext} from '#/state/preferences/moderation-opts'
 import {FeedNotification} from '#/state/queries/notifications/types'
 import {
   groupNotifications,
   shouldFilterNotif,
 } from '#/state/queries/notifications/util'
-import {moderationOptsOverrideContext} from '#/state/queries/preferences'
 import {useSession} from '#/state/session'
 import {CommonNavigatorParams, NativeStackScreenProps} from 'lib/routes/types'
 import {CenteredView, ScrollView} from '#/view/com/util/Views'
diff --git a/src/view/screens/Profile.tsx b/src/view/screens/Profile.tsx
index 02d7c90fb..4fa46a4cf 100644
--- a/src/view/screens/Profile.tsx
+++ b/src/view/screens/Profile.tsx
@@ -13,9 +13,9 @@ import {useQueryClient} from '@tanstack/react-query'
 
 import {cleanError} from '#/lib/strings/errors'
 import {useProfileShadow} from '#/state/cache/profile-shadow'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useLabelerInfoQuery} from '#/state/queries/labeler'
 import {resetProfilePostsQueries} from '#/state/queries/post-feed'
-import {useModerationOpts} from '#/state/queries/preferences'
 import {useProfileQuery} from '#/state/queries/profile'
 import {useResolveDidQuery} from '#/state/queries/resolve-uri'
 import {useAgent, useSession} from '#/state/session'
diff --git a/src/view/screens/Search/Search.tsx b/src/view/screens/Search/Search.tsx
index 9e34067fb..9dd1c397f 100644
--- a/src/view/screens/Search/Search.tsx
+++ b/src/view/screens/Search/Search.tsx
@@ -27,9 +27,9 @@ import {s} from '#/lib/styles'
 import {logger} from '#/logger'
 import {isIOS, isNative, isWeb} from '#/platform/detection'
 import {listenSoftReset} from '#/state/events'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
 import {useActorSearch} from '#/state/queries/actor-search'
-import {useModerationOpts} from '#/state/queries/preferences'
 import {useSearchPostsQuery} from '#/state/queries/search-posts'
 import {useSuggestedFollowsQuery} from '#/state/queries/suggested-follows'
 import {useSession} from '#/state/session'
diff --git a/src/view/shell/desktop/Search.tsx b/src/view/shell/desktop/Search.tsx
index 683d4421a..3829a6c0b 100644
--- a/src/view/shell/desktop/Search.tsx
+++ b/src/view/shell/desktop/Search.tsx
@@ -21,8 +21,8 @@ import {makeProfileLink} from '#/lib/routes/links'
 import {sanitizeDisplayName} from '#/lib/strings/display-names'
 import {sanitizeHandle} from '#/lib/strings/handles'
 import {s} from '#/lib/styles'
+import {useModerationOpts} from '#/state/preferences/moderation-opts'
 import {useActorAutocompleteQuery} from '#/state/queries/actor-autocomplete'
-import {useModerationOpts} from '#/state/queries/preferences'
 import {usePalette} from 'lib/hooks/usePalette'
 import {MagnifyingGlassIcon2} from 'lib/icons'
 import {NavigationProp} from 'lib/routes/types'