about summary refs log tree commit diff
path: root/src/view/screens
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/screens')
-rw-r--r--src/view/screens/PreferencesHomeFeed.tsx143
-rw-r--r--src/view/screens/PreferencesThreads.tsx154
-rw-r--r--src/view/screens/SavedFeeds.tsx317
-rw-r--r--src/view/screens/Settings.tsx9
4 files changed, 385 insertions, 238 deletions
diff --git a/src/view/screens/PreferencesHomeFeed.tsx b/src/view/screens/PreferencesHomeFeed.tsx
index da99dc16f..7b240ded0 100644
--- a/src/view/screens/PreferencesHomeFeed.tsx
+++ b/src/view/screens/PreferencesHomeFeed.tsx
@@ -4,7 +4,6 @@ import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Slider} from '@miblanchard/react-native-slider'
 import {Text} from '../com/util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -16,21 +15,31 @@ import {CenteredView} from 'view/com/util/Views'
 import debounce from 'lodash.debounce'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {
+  usePreferencesQuery,
+  useSetFeedViewPreferencesMutation,
+} from '#/state/queries/preferences'
 
-function RepliesThresholdInput({enabled}: {enabled: boolean}) {
-  const store = useStores()
+function RepliesThresholdInput({
+  enabled,
+  initialValue,
+}: {
+  enabled: boolean
+  initialValue: number
+}) {
   const pal = usePalette('default')
-  const [value, setValue] = useState(
-    store.preferences.homeFeed.hideRepliesByLikeCount,
-  )
+  const [value, setValue] = useState(initialValue)
+  const {mutate: setFeedViewPref} = useSetFeedViewPreferencesMutation()
   const save = React.useMemo(
     () =>
       debounce(
         threshold =>
-          store.preferences.setHomeFeedHideRepliesByLikeCount(threshold),
+          setFeedViewPref({
+            hideRepliesByLikeCount: threshold,
+          }),
         500,
       ), // debouce for 500ms
-    [store],
+    [setFeedViewPref],
   )
 
   return (
@@ -67,9 +76,15 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
   navigation,
 }: Props) {
   const pal = usePalette('default')
-  const store = useStores()
   const {_} = useLingui()
   const {isTabletOrDesktop} = useWebMediaQueries()
+  const {data: preferences} = usePreferencesQuery()
+  const {mutate: setFeedViewPref, variables} =
+    useSetFeedViewPreferencesMutation()
+
+  const showReplies = !(
+    variables?.hideReplies ?? preferences?.feedViewPrefs?.hideReplies
+  )
 
   return (
     <CenteredView
@@ -105,17 +120,20 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
             <ToggleButton
               testID="toggleRepliesBtn"
               type="default-light"
-              label={store.preferences.homeFeed.hideReplies ? 'No' : 'Yes'}
-              isSelected={!store.preferences.homeFeed.hideReplies}
-              onPress={store.preferences.toggleHomeFeedHideReplies}
+              label={showReplies ? 'Yes' : 'No'}
+              isSelected={showReplies}
+              onPress={() =>
+                setFeedViewPref({
+                  hideReplies: !(
+                    variables?.hideReplies ??
+                    preferences?.feedViewPrefs?.hideReplies
+                  ),
+                })
+              }
             />
           </View>
           <View
-            style={[
-              pal.viewLight,
-              styles.card,
-              store.preferences.homeFeed.hideReplies && styles.dimmed,
-            ]}>
+            style={[pal.viewLight, styles.card, !showReplies && styles.dimmed]}>
             <Text type="title-sm" style={[pal.text, s.pb5]}>
               <Trans>Reply Filters</Trans>
             </Text>
@@ -128,10 +146,19 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
             <ToggleButton
               type="default-light"
               label="Followed users only"
-              isSelected={store.preferences.homeFeed.hideRepliesByUnfollowed}
+              isSelected={Boolean(
+                variables?.hideRepliesByUnfollowed ??
+                  preferences?.feedViewPrefs?.hideRepliesByUnfollowed,
+              )}
               onPress={
-                !store.preferences.homeFeed.hideReplies
-                  ? store.preferences.toggleHomeFeedHideRepliesByUnfollowed
+                showReplies
+                  ? () =>
+                      setFeedViewPref({
+                        hideRepliesByUnfollowed: !(
+                          variables?.hideRepliesByUnfollowed ??
+                          preferences?.feedViewPrefs?.hideRepliesByUnfollowed
+                        ),
+                      })
                   : undefined
               }
               style={[s.mb10]}
@@ -142,9 +169,12 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
                 feed.
               </Trans>
             </Text>
-            <RepliesThresholdInput
-              enabled={!store.preferences.homeFeed.hideReplies}
-            />
+            {preferences && (
+              <RepliesThresholdInput
+                enabled={showReplies}
+                initialValue={preferences.feedViewPrefs.hideRepliesByLikeCount}
+              />
+            )}
           </View>
 
           <View style={[pal.viewLight, styles.card]}>
@@ -158,9 +188,26 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
             </Text>
             <ToggleButton
               type="default-light"
-              label={store.preferences.homeFeed.hideReposts ? 'No' : 'Yes'}
-              isSelected={!store.preferences.homeFeed.hideReposts}
-              onPress={store.preferences.toggleHomeFeedHideReposts}
+              label={
+                variables?.hideReposts ??
+                preferences?.feedViewPrefs?.hideReposts
+                  ? 'No'
+                  : 'Yes'
+              }
+              isSelected={
+                !(
+                  variables?.hideReposts ??
+                  preferences?.feedViewPrefs?.hideReposts
+                )
+              }
+              onPress={() =>
+                setFeedViewPref({
+                  hideReposts: !(
+                    variables?.hideReposts ??
+                    preferences?.feedViewPrefs?.hideReposts
+                  ),
+                })
+              }
             />
           </View>
 
@@ -176,9 +223,26 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
             </Text>
             <ToggleButton
               type="default-light"
-              label={store.preferences.homeFeed.hideQuotePosts ? 'No' : 'Yes'}
-              isSelected={!store.preferences.homeFeed.hideQuotePosts}
-              onPress={store.preferences.toggleHomeFeedHideQuotePosts}
+              label={
+                variables?.hideQuotePosts ??
+                preferences?.feedViewPrefs?.hideQuotePosts
+                  ? 'No'
+                  : 'Yes'
+              }
+              isSelected={
+                !(
+                  variables?.hideQuotePosts ??
+                  preferences?.feedViewPrefs?.hideQuotePosts
+                )
+              }
+              onPress={() =>
+                setFeedViewPref({
+                  hideQuotePosts: !(
+                    variables?.hideQuotePosts ??
+                    preferences?.feedViewPrefs?.hideQuotePosts
+                  ),
+                })
+              }
             />
           </View>
 
@@ -196,10 +260,25 @@ export const PreferencesHomeFeed = observer(function PreferencesHomeFeedImpl({
             <ToggleButton
               type="default-light"
               label={
-                store.preferences.homeFeed.lab_mergeFeedEnabled ? 'Yes' : 'No'
+                variables?.lab_mergeFeedEnabled ??
+                preferences?.feedViewPrefs?.lab_mergeFeedEnabled
+                  ? 'Yes'
+                  : 'No'
+              }
+              isSelected={
+                !!(
+                  variables?.lab_mergeFeedEnabled ??
+                  preferences?.feedViewPrefs?.lab_mergeFeedEnabled
+                )
+              }
+              onPress={() =>
+                setFeedViewPref({
+                  lab_mergeFeedEnabled: !(
+                    variables?.lab_mergeFeedEnabled ??
+                    preferences?.feedViewPrefs?.lab_mergeFeedEnabled
+                  ),
+                })
               }
-              isSelected={!!store.preferences.homeFeed.lab_mergeFeedEnabled}
-              onPress={store.preferences.toggleHomeFeedMergeFeedEnabled}
             />
           </View>
         </View>
diff --git a/src/view/screens/PreferencesThreads.tsx b/src/view/screens/PreferencesThreads.tsx
index 8a2db13ce..2386f6445 100644
--- a/src/view/screens/PreferencesThreads.tsx
+++ b/src/view/screens/PreferencesThreads.tsx
@@ -1,9 +1,14 @@
 import React from 'react'
-import {ScrollView, StyleSheet, TouchableOpacity, View} from 'react-native'
+import {
+  ActivityIndicator,
+  ScrollView,
+  StyleSheet,
+  TouchableOpacity,
+  View,
+} from 'react-native'
 import {observer} from 'mobx-react-lite'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import {Text} from '../com/util/text/Text'
-import {useStores} from 'state/index'
 import {s, colors} from 'lib/styles'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
@@ -14,15 +19,30 @@ import {ViewHeader} from 'view/com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
 import {Trans, msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {
+  usePreferencesQuery,
+  useSetThreadViewPreferencesMutation,
+} from '#/state/queries/preferences'
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'PreferencesThreads'>
 export const PreferencesThreads = observer(function PreferencesThreadsImpl({
   navigation,
 }: Props) {
   const pal = usePalette('default')
-  const store = useStores()
   const {_} = useLingui()
   const {isTabletOrDesktop} = useWebMediaQueries()
+  const {data: preferences} = usePreferencesQuery()
+  const {mutate: setThreadViewPrefs, variables} =
+    useSetThreadViewPreferencesMutation()
+
+  const prioritizeFollowedUsers = Boolean(
+    variables?.prioritizeFollowedUsers ??
+      preferences?.threadViewPrefs?.prioritizeFollowedUsers,
+  )
+  const treeViewEnabled = Boolean(
+    variables?.lab_treeViewEnabled ??
+      preferences?.threadViewPrefs?.lab_treeViewEnabled,
+  )
 
   return (
     <CenteredView
@@ -44,71 +64,79 @@ export const PreferencesThreads = observer(function PreferencesThreadsImpl({
         </Text>
       </View>
 
-      <ScrollView>
-        <View style={styles.cardsContainer}>
-          <View style={[pal.viewLight, styles.card]}>
-            <Text type="title-sm" style={[pal.text, s.pb5]}>
-              <Trans>Sort Replies</Trans>
-            </Text>
-            <Text style={[pal.text, s.pb10]}>
-              <Trans>Sort replies to the same post by:</Trans>
-            </Text>
-            <View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
-              <RadioGroup
+      {preferences ? (
+        <ScrollView>
+          <View style={styles.cardsContainer}>
+            <View style={[pal.viewLight, styles.card]}>
+              <Text type="title-sm" style={[pal.text, s.pb5]}>
+                <Trans>Sort Replies</Trans>
+              </Text>
+              <Text style={[pal.text, s.pb10]}>
+                <Trans>Sort replies to the same post by:</Trans>
+              </Text>
+              <View style={[pal.view, {borderRadius: 8, paddingVertical: 6}]}>
+                <RadioGroup
+                  type="default-light"
+                  items={[
+                    {key: 'oldest', label: 'Oldest replies first'},
+                    {key: 'newest', label: 'Newest replies first'},
+                    {key: 'most-likes', label: 'Most-liked replies first'},
+                    {key: 'random', label: 'Random (aka "Poster\'s Roulette")'},
+                  ]}
+                  onSelect={key => setThreadViewPrefs({sort: key})}
+                  initialSelection={preferences?.threadViewPrefs?.sort}
+                />
+              </View>
+            </View>
+
+            <View style={[pal.viewLight, styles.card]}>
+              <Text type="title-sm" style={[pal.text, s.pb5]}>
+                <Trans>Prioritize Your Follows</Trans>
+              </Text>
+              <Text style={[pal.text, s.pb10]}>
+                <Trans>
+                  Show replies by people you follow before all other replies.
+                </Trans>
+              </Text>
+              <ToggleButton
                 type="default-light"
-                items={[
-                  {key: 'oldest', label: 'Oldest replies first'},
-                  {key: 'newest', label: 'Newest replies first'},
-                  {key: 'most-likes', label: 'Most-liked replies first'},
-                  {key: 'random', label: 'Random (aka "Poster\'s Roulette")'},
-                ]}
-                onSelect={store.preferences.setThreadSort}
-                initialSelection={store.preferences.thread.sort}
+                label={prioritizeFollowedUsers ? 'Yes' : 'No'}
+                isSelected={prioritizeFollowedUsers}
+                onPress={() =>
+                  setThreadViewPrefs({
+                    prioritizeFollowedUsers: !prioritizeFollowedUsers,
+                  })
+                }
               />
             </View>
-          </View>
 
-          <View style={[pal.viewLight, styles.card]}>
-            <Text type="title-sm" style={[pal.text, s.pb5]}>
-              <Trans>Prioritize Your Follows</Trans>
-            </Text>
-            <Text style={[pal.text, s.pb10]}>
-              <Trans>
-                Show replies by people you follow before all other replies.
-              </Trans>
-            </Text>
-            <ToggleButton
-              type="default-light"
-              label={
-                store.preferences.thread.prioritizeFollowedUsers ? 'Yes' : 'No'
-              }
-              isSelected={store.preferences.thread.prioritizeFollowedUsers}
-              onPress={store.preferences.togglePrioritizedFollowedUsers}
-            />
-          </View>
-
-          <View style={[pal.viewLight, styles.card]}>
-            <Text type="title-sm" style={[pal.text, s.pb5]}>
-              <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
-              <Trans>Threaded Mode</Trans>
-            </Text>
-            <Text style={[pal.text, s.pb10]}>
-              <Trans>
-                Set this setting to "Yes" to show replies in a threaded view.
-                This is an experimental feature.
-              </Trans>
-            </Text>
-            <ToggleButton
-              type="default-light"
-              label={
-                store.preferences.thread.lab_treeViewEnabled ? 'Yes' : 'No'
-              }
-              isSelected={!!store.preferences.thread.lab_treeViewEnabled}
-              onPress={store.preferences.toggleThreadTreeViewEnabled}
-            />
+            <View style={[pal.viewLight, styles.card]}>
+              <Text type="title-sm" style={[pal.text, s.pb5]}>
+                <FontAwesomeIcon icon="flask" color={pal.colors.text} />{' '}
+                <Trans>Threaded Mode</Trans>
+              </Text>
+              <Text style={[pal.text, s.pb10]}>
+                <Trans>
+                  Set this setting to "Yes" to show replies in a threaded view.
+                  This is an experimental feature.
+                </Trans>
+              </Text>
+              <ToggleButton
+                type="default-light"
+                label={treeViewEnabled ? 'Yes' : 'No'}
+                isSelected={treeViewEnabled}
+                onPress={() =>
+                  setThreadViewPrefs({
+                    lab_treeViewEnabled: !treeViewEnabled,
+                  })
+                }
+              />
+            </View>
           </View>
-        </View>
-      </ScrollView>
+        </ScrollView>
+      ) : (
+        <ActivityIndicator />
+      )}
 
       <View
         style={[
diff --git a/src/view/screens/SavedFeeds.tsx b/src/view/screens/SavedFeeds.tsx
index 487f56643..8ca2383d2 100644
--- a/src/view/screens/SavedFeeds.tsx
+++ b/src/view/screens/SavedFeeds.tsx
@@ -1,4 +1,4 @@
-import React, {useCallback, useMemo} from 'react'
+import React from 'react'
 import {
   StyleSheet,
   View,
@@ -8,26 +8,34 @@ import {
 } from 'react-native'
 import {useFocusEffect} from '@react-navigation/native'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
+import {useQueryClient} from '@tanstack/react-query'
+
+import {track} from '#/lib/analytics/analytics'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {usePalette} from 'lib/hooks/usePalette'
 import {CommonNavigatorParams} from 'lib/routes/types'
 import {observer} from 'mobx-react-lite'
-import {useStores} from 'state/index'
-import {SavedFeedsModel} from 'state/models/ui/saved-feeds'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
 import {ViewHeader} from 'view/com/util/ViewHeader'
 import {ScrollView, CenteredView} from 'view/com/util/Views'
 import {Text} from 'view/com/util/text/Text'
 import {s, colors} from 'lib/styles'
-import {FeedSourceCard} from 'view/com/feeds/FeedSourceCard'
-import {FeedSourceModel} from 'state/models/content/feed-source'
+import {NewFeedSourceCard} from 'view/com/feeds/FeedSourceCard'
 import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
 import * as Toast from 'view/com/util/Toast'
 import {Haptics} from 'lib/haptics'
 import {TextLink} from 'view/com/util/Link'
 import {logger} from '#/logger'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {
+  usePreferencesQuery,
+  usePinFeedMutation,
+  useUnpinFeedMutation,
+  useSetSaveFeedsMutation,
+  usePreferencesQueryKey,
+  UsePreferencesQueryResponse,
+} from '#/state/queries/preferences'
 
 const HITSLOP_TOP = {
   top: 20,
@@ -43,150 +51,178 @@ const HITSLOP_BOTTOM = {
 }
 
 type Props = NativeStackScreenProps<CommonNavigatorParams, 'SavedFeeds'>
-export const SavedFeeds = withAuthRequired(
-  observer(function SavedFeedsImpl({}: Props) {
-    const pal = usePalette('default')
-    const store = useStores()
-    const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
-    const {screen} = useAnalytics()
-    const setMinimalShellMode = useSetMinimalShellMode()
+export const SavedFeeds = withAuthRequired(function SavedFeedsImpl({}: Props) {
+  const pal = usePalette('default')
+  const {isMobile, isTabletOrDesktop} = useWebMediaQueries()
+  const {screen} = useAnalytics()
+  const setMinimalShellMode = useSetMinimalShellMode()
+  const {data: preferences} = usePreferencesQuery()
 
-    const savedFeeds = useMemo(() => {
-      const model = new SavedFeedsModel(store)
-      model.refresh()
-      return model
-    }, [store])
-    useFocusEffect(
-      useCallback(() => {
-        screen('SavedFeeds')
-        setMinimalShellMode(false)
-        savedFeeds.refresh()
-      }, [screen, setMinimalShellMode, savedFeeds]),
-    )
+  useFocusEffect(
+    React.useCallback(() => {
+      screen('SavedFeeds')
+      setMinimalShellMode(false)
+    }, [screen, setMinimalShellMode]),
+  )
 
-    return (
-      <CenteredView
-        style={[
-          s.hContentRegion,
-          pal.border,
-          isTabletOrDesktop && styles.desktopContainer,
-        ]}>
-        <ViewHeader title="Edit My Feeds" showOnDesktop showBorder />
-        <ScrollView style={s.flex1}>
-          <View style={[pal.text, pal.border, styles.title]}>
-            <Text type="title" style={pal.text}>
-              Pinned Feeds
-            </Text>
-          </View>
-          {savedFeeds.hasLoaded ? (
-            !savedFeeds.pinned.length ? (
-              <View
-                style={[
-                  pal.border,
-                  isMobile && s.flex1,
-                  pal.viewLight,
-                  styles.empty,
-                ]}>
-                <Text type="lg" style={[pal.text]}>
-                  You don't have any pinned feeds.
-                </Text>
-              </View>
-            ) : (
-              savedFeeds.pinned.map(feed => (
-                <ListItem
-                  key={feed._reactKey}
-                  savedFeeds={savedFeeds}
-                  item={feed}
-                />
-              ))
-            )
+  return (
+    <CenteredView
+      style={[
+        s.hContentRegion,
+        pal.border,
+        isTabletOrDesktop && styles.desktopContainer,
+      ]}>
+      <ViewHeader title="Edit My Feeds" showOnDesktop showBorder />
+      <ScrollView style={s.flex1}>
+        <View style={[pal.text, pal.border, styles.title]}>
+          <Text type="title" style={pal.text}>
+            Pinned Feeds
+          </Text>
+        </View>
+        {preferences?.feeds ? (
+          !preferences.feeds.pinned.length ? (
+            <View
+              style={[
+                pal.border,
+                isMobile && s.flex1,
+                pal.viewLight,
+                styles.empty,
+              ]}>
+              <Text type="lg" style={[pal.text]}>
+                You don't have any pinned feeds.
+              </Text>
+            </View>
           ) : (
-            <ActivityIndicator style={{marginTop: 20}} />
-          )}
-          <View style={[pal.text, pal.border, styles.title]}>
-            <Text type="title" style={pal.text}>
-              Saved Feeds
-            </Text>
-          </View>
-          {savedFeeds.hasLoaded ? (
-            !savedFeeds.unpinned.length ? (
-              <View
-                style={[
-                  pal.border,
-                  isMobile && s.flex1,
-                  pal.viewLight,
-                  styles.empty,
-                ]}>
-                <Text type="lg" style={[pal.text]}>
-                  You don't have any saved feeds.
-                </Text>
-              </View>
-            ) : (
-              savedFeeds.unpinned.map(feed => (
-                <ListItem
-                  key={feed._reactKey}
-                  savedFeeds={savedFeeds}
-                  item={feed}
-                />
-              ))
-            )
+            preferences?.feeds?.pinned?.map(uri => (
+              <ListItem key={uri} feedUri={uri} isPinned />
+            ))
+          )
+        ) : (
+          <ActivityIndicator style={{marginTop: 20}} />
+        )}
+        <View style={[pal.text, pal.border, styles.title]}>
+          <Text type="title" style={pal.text}>
+            Saved Feeds
+          </Text>
+        </View>
+        {preferences?.feeds ? (
+          !preferences.feeds.unpinned.length ? (
+            <View
+              style={[
+                pal.border,
+                isMobile && s.flex1,
+                pal.viewLight,
+                styles.empty,
+              ]}>
+              <Text type="lg" style={[pal.text]}>
+                You don't have any saved feeds.
+              </Text>
+            </View>
           ) : (
-            <ActivityIndicator style={{marginTop: 20}} />
-          )}
+            preferences.feeds.unpinned.map(uri => (
+              <ListItem key={uri} feedUri={uri} isPinned={false} />
+            ))
+          )
+        ) : (
+          <ActivityIndicator style={{marginTop: 20}} />
+        )}
 
-          <View style={styles.footerText}>
-            <Text type="sm" style={pal.textLight}>
-              Feeds are custom algorithms that users build with a little coding
-              expertise.{' '}
-              <TextLink
-                type="sm"
-                style={pal.link}
-                href="https://github.com/bluesky-social/feed-generator"
-                text="See this guide"
-              />{' '}
-              for more information.
-            </Text>
-          </View>
-          <View style={{height: 100}} />
-        </ScrollView>
-      </CenteredView>
-    )
-  }),
-)
+        <View style={styles.footerText}>
+          <Text type="sm" style={pal.textLight}>
+            Feeds are custom algorithms that users build with a little coding
+            expertise.{' '}
+            <TextLink
+              type="sm"
+              style={pal.link}
+              href="https://github.com/bluesky-social/feed-generator"
+              text="See this guide"
+            />{' '}
+            for more information.
+          </Text>
+        </View>
+        <View style={{height: 100}} />
+      </ScrollView>
+    </CenteredView>
+  )
+})
 
 const ListItem = observer(function ListItemImpl({
-  savedFeeds,
-  item,
+  feedUri,
+  isPinned,
 }: {
-  savedFeeds: SavedFeedsModel
-  item: FeedSourceModel
+  feedUri: string // uri
+  isPinned: boolean
 }) {
   const pal = usePalette('default')
-  const isPinned = item.isPinned
+  const queryClient = useQueryClient()
+  const {isPending: isPinPending, mutateAsync: pinFeed} = usePinFeedMutation()
+  const {isPending: isUnpinPending, mutateAsync: unpinFeed} =
+    useUnpinFeedMutation()
+  const {isPending: isMovePending, mutateAsync: setSavedFeeds} =
+    useSetSaveFeedsMutation()
 
-  const onTogglePinned = useCallback(() => {
+  const onTogglePinned = React.useCallback(async () => {
     Haptics.default()
-    item.togglePin().catch(e => {
+
+    try {
+      if (isPinned) {
+        await unpinFeed({uri: feedUri})
+      } else {
+        await pinFeed({uri: feedUri})
+      }
+    } catch (e) {
       Toast.show('There was an issue contacting the server')
       logger.error('Failed to toggle pinned feed', {error: e})
-    })
-  }, [item])
-  const onPressUp = useCallback(
-    () =>
-      savedFeeds.movePinnedFeed(item, 'up').catch(e => {
-        Toast.show('There was an issue contacting the server')
-        logger.error('Failed to set pinned feed order', {error: e})
-      }),
-    [savedFeeds, item],
-  )
-  const onPressDown = useCallback(
-    () =>
-      savedFeeds.movePinnedFeed(item, 'down').catch(e => {
-        Toast.show('There was an issue contacting the server')
-        logger.error('Failed to set pinned feed order', {error: e})
-      }),
-    [savedFeeds, item],
-  )
+    }
+  }, [feedUri, isPinned, pinFeed, unpinFeed])
+
+  const onPressUp = React.useCallback(async () => {
+    if (!isPinned) return
+
+    const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>(
+      usePreferencesQueryKey,
+    )?.feeds
+    const pinned = feeds?.pinned ?? []
+    const index = pinned.indexOf(feedUri)
+
+    if (index === -1 || index === 0) return
+    ;[pinned[index], pinned[index - 1]] = [pinned[index - 1], pinned[index]]
+
+    try {
+      await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
+      track('CustomFeed:Reorder', {
+        uri: feedUri,
+        index: pinned.indexOf(feedUri),
+      })
+    } catch (e) {
+      Toast.show('There was an issue contacting the server')
+      logger.error('Failed to set pinned feed order', {error: e})
+    }
+  }, [feedUri, isPinned, queryClient, setSavedFeeds])
+
+  const onPressDown = React.useCallback(async () => {
+    if (!isPinned) return
+
+    const feeds = queryClient.getQueryData<UsePreferencesQueryResponse>(
+      usePreferencesQueryKey,
+    )?.feeds
+    const pinned = feeds?.pinned ?? []
+    const index = pinned.indexOf(feedUri)
+
+    if (index === -1 || index >= pinned.length - 1) return
+    ;[pinned[index], pinned[index + 1]] = [pinned[index + 1], pinned[index]]
+
+    try {
+      await setSavedFeeds({saved: feeds?.saved ?? [], pinned})
+      track('CustomFeed:Reorder', {
+        uri: feedUri,
+        index: pinned.indexOf(feedUri),
+      })
+    } catch (e) {
+      Toast.show('There was an issue contacting the server')
+      logger.error('Failed to set pinned feed order', {error: e})
+    }
+  }, [feedUri, isPinned, queryClient, setSavedFeeds])
 
   return (
     <Pressable
@@ -195,6 +231,7 @@ const ListItem = observer(function ListItemImpl({
       {isPinned ? (
         <View style={styles.webArrowButtonsContainer}>
           <TouchableOpacity
+            disabled={isMovePending}
             accessibilityRole="button"
             onPress={onPressUp}
             hitSlop={HITSLOP_TOP}>
@@ -205,6 +242,7 @@ const ListItem = observer(function ListItemImpl({
             />
           </TouchableOpacity>
           <TouchableOpacity
+            disabled={isMovePending}
             accessibilityRole="button"
             onPress={onPressDown}
             hitSlop={HITSLOP_BOTTOM}>
@@ -212,13 +250,14 @@ const ListItem = observer(function ListItemImpl({
           </TouchableOpacity>
         </View>
       ) : null}
-      <FeedSourceCard
-        key={item.uri}
-        item={item}
-        showSaveBtn
+      <NewFeedSourceCard
+        key={feedUri}
+        feedUri={feedUri}
         style={styles.noBorder}
+        showSaveBtn
       />
       <TouchableOpacity
+        disabled={isPinPending || isUnpinPending}
         accessibilityRole="button"
         hitSlop={10}
         onPress={onTogglePinned}>
diff --git a/src/view/screens/Settings.tsx b/src/view/screens/Settings.tsx
index e56a50d79..baad2227b 100644
--- a/src/view/screens/Settings.tsx
+++ b/src/view/screens/Settings.tsx
@@ -59,6 +59,7 @@ import {
 } from '#/state/preferences'
 import {useSession, useSessionApi, SessionAccount} from '#/state/session'
 import {useProfileQuery} from '#/state/queries/profile'
+import {useClearPreferencesMutation} from '#/state/queries/preferences'
 
 // TEMPORARY (APP-700)
 // remove after backend testing finishes
@@ -153,6 +154,7 @@ export const SettingsScreen = withAuthRequired(
     const {openModal} = useModalControls()
     const {isSwitchingAccounts, accounts, currentAccount} = useSession()
     const {clearCurrentAccount} = useSessionApi()
+    const {mutate: clearPreferences} = useClearPreferencesMutation()
 
     const primaryBg = useCustomPalette<ViewStyle>({
       light: {backgroundColor: colors.blue0},
@@ -219,9 +221,8 @@ export const SettingsScreen = withAuthRequired(
     }, [openModal])
 
     const onPressResetPreferences = React.useCallback(async () => {
-      await store.preferences.reset()
-      Toast.show('Preferences reset')
-    }, [store])
+      clearPreferences()
+    }, [clearPreferences])
 
     const onPressResetOnboarding = React.useCallback(async () => {
       onboardingDispatch({type: 'start'})
@@ -300,7 +301,7 @@ export const SettingsScreen = withAuthRequired(
               </View>
               <View style={[styles.infoLine]}>
                 <Text type="lg-medium" style={pal.text}>
-                  <Trans>Birthday: </Trans>
+                  <Trans>Birthday:</Trans>{' '}
                 </Text>
                 <Link onPress={() => openModal({name: 'birth-date-settings'})}>
                   <Text type="lg" style={pal.link}>