about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--src/state/models/lists/blocked-accounts.ts107
-rw-r--r--src/state/models/lists/muted-accounts.ts107
-rw-r--r--src/state/queries/my-blocked-accounts.ts28
-rw-r--r--src/state/queries/my-muted-accounts.ts28
-rw-r--r--src/state/queries/profile.ts8
-rw-r--r--src/view/com/lists/ListMembers.tsx3
-rw-r--r--src/view/com/profile/ProfileCard.tsx19
-rw-r--r--src/view/screens/ModerationBlockedAccounts.tsx102
-rw-r--r--src/view/screens/ModerationMutedAccounts.tsx99
9 files changed, 212 insertions, 289 deletions
diff --git a/src/state/models/lists/blocked-accounts.ts b/src/state/models/lists/blocked-accounts.ts
deleted file mode 100644
index 5c3dbe7ce..000000000
--- a/src/state/models/lists/blocked-accounts.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {
-  AppBskyGraphGetBlocks as GetBlocks,
-  AppBskyActorDefs as ActorDefs,
-} from '@atproto/api'
-import {RootStoreModel} from '../root-store'
-import {cleanError} from 'lib/strings/errors'
-import {bundleAsync} from 'lib/async/bundle'
-import {logger} from '#/logger'
-
-const PAGE_SIZE = 30
-
-export class BlockedAccountsModel {
-  // state
-  isLoading = false
-  isRefreshing = false
-  hasLoaded = false
-  error = ''
-  hasMore = true
-  loadMoreCursor?: string
-
-  // data
-  blocks: ActorDefs.ProfileView[] = []
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  get hasContent() {
-    return this.blocks.length > 0
-  }
-
-  get hasError() {
-    return this.error !== ''
-  }
-
-  get isEmpty() {
-    return this.hasLoaded && !this.hasContent
-  }
-
-  // public api
-  // =
-
-  async refresh() {
-    return this.loadMore(true)
-  }
-
-  loadMore = bundleAsync(async (replace: boolean = false) => {
-    if (!replace && !this.hasMore) {
-      return
-    }
-    this._xLoading(replace)
-    try {
-      const res = await this.rootStore.agent.app.bsky.graph.getBlocks({
-        limit: PAGE_SIZE,
-        cursor: replace ? undefined : this.loadMoreCursor,
-      })
-      if (replace) {
-        this._replaceAll(res)
-      } else {
-        this._appendAll(res)
-      }
-      this._xIdle()
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-  })
-
-  // state transitions
-  // =
-
-  _xLoading(isRefreshing = false) {
-    this.isLoading = true
-    this.isRefreshing = isRefreshing
-    this.error = ''
-  }
-
-  _xIdle(err?: any) {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = true
-    this.error = cleanError(err)
-    if (err) {
-      logger.error('Failed to fetch user followers', {error: err})
-    }
-  }
-
-  // helper functions
-  // =
-
-  _replaceAll(res: GetBlocks.Response) {
-    this.blocks = []
-    this._appendAll(res)
-  }
-
-  _appendAll(res: GetBlocks.Response) {
-    this.loadMoreCursor = res.data.cursor
-    this.hasMore = !!this.loadMoreCursor
-    this.blocks = this.blocks.concat(res.data.blocks)
-  }
-}
diff --git a/src/state/models/lists/muted-accounts.ts b/src/state/models/lists/muted-accounts.ts
deleted file mode 100644
index 19ade0d9c..000000000
--- a/src/state/models/lists/muted-accounts.ts
+++ /dev/null
@@ -1,107 +0,0 @@
-import {makeAutoObservable} from 'mobx'
-import {
-  AppBskyGraphGetMutes as GetMutes,
-  AppBskyActorDefs as ActorDefs,
-} from '@atproto/api'
-import {RootStoreModel} from '../root-store'
-import {cleanError} from 'lib/strings/errors'
-import {bundleAsync} from 'lib/async/bundle'
-import {logger} from '#/logger'
-
-const PAGE_SIZE = 30
-
-export class MutedAccountsModel {
-  // state
-  isLoading = false
-  isRefreshing = false
-  hasLoaded = false
-  error = ''
-  hasMore = true
-  loadMoreCursor?: string
-
-  // data
-  mutes: ActorDefs.ProfileView[] = []
-
-  constructor(public rootStore: RootStoreModel) {
-    makeAutoObservable(
-      this,
-      {
-        rootStore: false,
-      },
-      {autoBind: true},
-    )
-  }
-
-  get hasContent() {
-    return this.mutes.length > 0
-  }
-
-  get hasError() {
-    return this.error !== ''
-  }
-
-  get isEmpty() {
-    return this.hasLoaded && !this.hasContent
-  }
-
-  // public api
-  // =
-
-  async refresh() {
-    return this.loadMore(true)
-  }
-
-  loadMore = bundleAsync(async (replace: boolean = false) => {
-    if (!replace && !this.hasMore) {
-      return
-    }
-    this._xLoading(replace)
-    try {
-      const res = await this.rootStore.agent.app.bsky.graph.getMutes({
-        limit: PAGE_SIZE,
-        cursor: replace ? undefined : this.loadMoreCursor,
-      })
-      if (replace) {
-        this._replaceAll(res)
-      } else {
-        this._appendAll(res)
-      }
-      this._xIdle()
-    } catch (e: any) {
-      this._xIdle(e)
-    }
-  })
-
-  // state transitions
-  // =
-
-  _xLoading(isRefreshing = false) {
-    this.isLoading = true
-    this.isRefreshing = isRefreshing
-    this.error = ''
-  }
-
-  _xIdle(err?: any) {
-    this.isLoading = false
-    this.isRefreshing = false
-    this.hasLoaded = true
-    this.error = cleanError(err)
-    if (err) {
-      logger.error('Failed to fetch user followers', {error: err})
-    }
-  }
-
-  // helper functions
-  // =
-
-  _replaceAll(res: GetMutes.Response) {
-    this.mutes = []
-    this._appendAll(res)
-  }
-
-  _appendAll(res: GetMutes.Response) {
-    this.loadMoreCursor = res.data.cursor
-    this.hasMore = !!this.loadMoreCursor
-    this.mutes = this.mutes.concat(res.data.mutes)
-  }
-}
diff --git a/src/state/queries/my-blocked-accounts.ts b/src/state/queries/my-blocked-accounts.ts
new file mode 100644
index 000000000..448f7dd67
--- /dev/null
+++ b/src/state/queries/my-blocked-accounts.ts
@@ -0,0 +1,28 @@
+import {AppBskyGraphGetBlocks} from '@atproto/api'
+import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
+import {useSession} from '../session'
+
+export const RQKEY = () => ['my-blocked-accounts']
+type RQPageParam = string | undefined
+
+export function useMyBlockedAccountsQuery() {
+  const {agent} = useSession()
+  return useInfiniteQuery<
+    AppBskyGraphGetBlocks.OutputSchema,
+    Error,
+    InfiniteData<AppBskyGraphGetBlocks.OutputSchema>,
+    QueryKey,
+    RQPageParam
+  >({
+    queryKey: RQKEY(),
+    async queryFn({pageParam}: {pageParam: RQPageParam}) {
+      const res = await agent.app.bsky.graph.getBlocks({
+        limit: 30,
+        cursor: pageParam,
+      })
+      return res.data
+    },
+    initialPageParam: undefined,
+    getNextPageParam: lastPage => lastPage.cursor,
+  })
+}
diff --git a/src/state/queries/my-muted-accounts.ts b/src/state/queries/my-muted-accounts.ts
new file mode 100644
index 000000000..109874673
--- /dev/null
+++ b/src/state/queries/my-muted-accounts.ts
@@ -0,0 +1,28 @@
+import {AppBskyGraphGetMutes} from '@atproto/api'
+import {useInfiniteQuery, InfiniteData, QueryKey} from '@tanstack/react-query'
+import {useSession} from '../session'
+
+export const RQKEY = () => ['my-muted-accounts']
+type RQPageParam = string | undefined
+
+export function useMyMutedAccountsQuery() {
+  const {agent} = useSession()
+  return useInfiniteQuery<
+    AppBskyGraphGetMutes.OutputSchema,
+    Error,
+    InfiniteData<AppBskyGraphGetMutes.OutputSchema>,
+    QueryKey,
+    RQPageParam
+  >({
+    queryKey: RQKEY(),
+    async queryFn({pageParam}: {pageParam: RQPageParam}) {
+      const res = await agent.app.bsky.graph.getMutes({
+        limit: 30,
+        cursor: pageParam,
+      })
+      return res.data
+    },
+    initialPageParam: undefined,
+    getNextPageParam: lastPage => lastPage.cursor,
+  })
+}
diff --git a/src/state/queries/profile.ts b/src/state/queries/profile.ts
index 63367b261..de2b1d65c 100644
--- a/src/state/queries/profile.ts
+++ b/src/state/queries/profile.ts
@@ -11,6 +11,8 @@ import {useSession} from '../session'
 import {updateProfileShadow} from '../cache/profile-shadow'
 import {uploadBlob} from '#/lib/api'
 import {until} from '#/lib/async/until'
+import {RQKEY as RQKEY_MY_MUTED} from './my-muted-accounts'
+import {RQKEY as RQKEY_MY_BLOCKED} from './my-blocked-accounts'
 
 export const RQKEY = (did: string) => ['profile', did]
 
@@ -147,6 +149,7 @@ export function useProfileUnfollowMutation() {
 
 export function useProfileMuteMutation() {
   const {agent} = useSession()
+  const queryClient = useQueryClient()
   return useMutation<void, Error, {did: string}>({
     mutationFn: async ({did}) => {
       await agent.mute(did)
@@ -157,6 +160,9 @@ export function useProfileMuteMutation() {
         muted: true,
       })
     },
+    onSuccess() {
+      queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})
+    },
     onError(error, variables) {
       // revert the optimistic update
       updateProfileShadow(variables.did, {
@@ -189,6 +195,7 @@ export function useProfileUnmuteMutation() {
 
 export function useProfileBlockMutation() {
   const {agent, currentAccount} = useSession()
+  const queryClient = useQueryClient()
   return useMutation<{uri: string; cid: string}, Error, {did: string}>({
     mutationFn: async ({did}) => {
       if (!currentAccount) {
@@ -210,6 +217,7 @@ export function useProfileBlockMutation() {
       updateProfileShadow(variables.did, {
         blockingUri: data.uri,
       })
+      queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()})
     },
     onError(error, variables) {
       // revert the optimistic update
diff --git a/src/view/com/lists/ListMembers.tsx b/src/view/com/lists/ListMembers.tsx
index 4a25c53e6..940761e31 100644
--- a/src/view/com/lists/ListMembers.tsx
+++ b/src/view/com/lists/ListMembers.tsx
@@ -64,6 +64,7 @@ export function ListMembers({
 
   const {
     data,
+    dataUpdatedAt,
     isFetching,
     isFetched,
     isError,
@@ -184,6 +185,7 @@ export function ListMembers({
             (item as AppBskyGraphDefs.ListItemView).subject.handle
           }`}
           profile={(item as AppBskyGraphDefs.ListItemView).subject}
+          dataUpdatedAt={dataUpdatedAt}
           renderButton={renderMemberButton}
           style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
         />
@@ -196,6 +198,7 @@ export function ListMembers({
       onPressTryAgain,
       onPressRetryLoadMore,
       isMobile,
+      dataUpdatedAt,
     ],
   )
 
diff --git a/src/view/com/profile/ProfileCard.tsx b/src/view/com/profile/ProfileCard.tsx
index f7340fd6f..95f0ecd93 100644
--- a/src/view/com/profile/ProfileCard.tsx
+++ b/src/view/com/profile/ProfileCard.tsx
@@ -21,10 +21,13 @@ import {
   getProfileModerationCauses,
   getModerationCauseKey,
 } from 'lib/moderation'
+import {useModerationOpts} from '#/state/queries/preferences'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
 
-export const ProfileCard = observer(function ProfileCardImpl({
+export function ProfileCard({
   testID,
-  profile,
+  profile: profileUnshadowed,
+  dataUpdatedAt,
   noBg,
   noBorder,
   followers,
@@ -33,16 +36,20 @@ export const ProfileCard = observer(function ProfileCardImpl({
 }: {
   testID?: string
   profile: AppBskyActorDefs.ProfileViewBasic
+  dataUpdatedAt: number
   noBg?: boolean
   noBorder?: boolean
   followers?: AppBskyActorDefs.ProfileView[] | undefined
   renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => React.ReactNode
   style?: StyleProp<ViewStyle>
 }) {
-  const store = useStores()
   const pal = usePalette('default')
-
-  const moderation = moderateProfile(profile, store.preferences.moderationOpts)
+  const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt)
+  const moderationOpts = useModerationOpts()
+  if (!moderationOpts) {
+    return null
+  }
+  const moderation = moderateProfile(profile, moderationOpts)
 
   return (
     <Link
@@ -100,7 +107,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
       <FollowersList followers={followers} />
     </Link>
   )
-})
+}
 
 function ProfileCardPills({
   followedBy,
diff --git a/src/view/screens/ModerationBlockedAccounts.tsx b/src/view/screens/ModerationBlockedAccounts.tsx
index 0dc3b706b..702a8d44e 100644
--- a/src/view/screens/ModerationBlockedAccounts.tsx
+++ b/src/view/screens/ModerationBlockedAccounts.tsx
@@ -1,4 +1,4 @@
-import React, {useMemo} from 'react'
+import React from 'react'
 import {
   ActivityIndicator,
   FlatList,
@@ -8,56 +8,78 @@ import {
 } from 'react-native'
 import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
 import {Text} from '../com/util/text/Text'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {CommonNavigatorParams} from 'lib/routes/types'
-import {BlockedAccountsModel} from 'state/models/lists/blocked-accounts'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useFocusEffect} from '@react-navigation/native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
+import {ErrorScreen} from '../com/util/error/ErrorScreen'
 import {ProfileCard} from 'view/com/profile/ProfileCard'
 import {logger} from '#/logger'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts'
+import {cleanError} from '#/lib/strings/errors'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
   'ModerationBlockedAccounts'
 >
 export const ModerationBlockedAccounts = withAuthRequired(
-  observer(function ModerationBlockedAccountsImpl({}: Props) {
+  function ModerationBlockedAccountsImpl({}: Props) {
     const pal = usePalette('default')
-    const store = useStores()
     const setMinimalShellMode = useSetMinimalShellMode()
     const {isTabletOrDesktop} = useWebMediaQueries()
     const {screen} = useAnalytics()
-    const blockedAccounts = useMemo(
-      () => new BlockedAccountsModel(store),
-      [store],
-    )
+    const [isPTRing, setIsPTRing] = React.useState(false)
+    const {
+      data,
+      dataUpdatedAt,
+      isFetching,
+      isError,
+      error,
+      refetch,
+      hasNextPage,
+      fetchNextPage,
+      isFetchingNextPage,
+    } = useMyBlockedAccountsQuery()
+    const isEmpty = !isFetching && !data?.pages[0]?.blocks.length
+    const profiles = React.useMemo(() => {
+      if (data?.pages) {
+        return data.pages.flatMap(page => page.blocks)
+      }
+      return []
+    }, [data])
 
     useFocusEffect(
       React.useCallback(() => {
         screen('BlockedAccounts')
         setMinimalShellMode(false)
-        blockedAccounts.refresh()
-      }, [screen, setMinimalShellMode, blockedAccounts]),
+      }, [screen, setMinimalShellMode]),
     )
 
-    const onRefresh = React.useCallback(() => {
-      blockedAccounts.refresh()
-    }, [blockedAccounts])
-    const onEndReached = React.useCallback(() => {
-      blockedAccounts
-        .loadMore()
-        .catch(err =>
-          logger.error('Failed to load more blocked accounts', {error: err}),
-        )
-    }, [blockedAccounts])
+    const onRefresh = React.useCallback(async () => {
+      setIsPTRing(true)
+      try {
+        await refetch()
+      } catch (err) {
+        logger.error('Failed to refresh my muted accounts', {error: err})
+      }
+      setIsPTRing(false)
+    }, [refetch, setIsPTRing])
+
+    const onEndReached = React.useCallback(async () => {
+      if (isFetching || !hasNextPage || isError) return
+
+      try {
+        await fetchNextPage()
+      } catch (err) {
+        logger.error('Failed to load more of my muted accounts', {error: err})
+      }
+    }, [isFetching, hasNextPage, isError, fetchNextPage])
 
     const renderItem = ({
       item,
@@ -70,6 +92,7 @@ export const ModerationBlockedAccounts = withAuthRequired(
         testID={`blockedAccount-${index}`}
         key={item.did}
         profile={item}
+        dataUpdatedAt={dataUpdatedAt}
       />
     )
     return (
@@ -93,24 +116,32 @@ export const ModerationBlockedAccounts = withAuthRequired(
           otherwise interact with you. You will not see their content and they
           will be prevented from seeing yours.
         </Text>
-        {!blockedAccounts.hasContent ? (
+        {isEmpty ? (
           <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
-            <View style={[styles.empty, pal.viewLight]}>
-              <Text type="lg" style={[pal.text, styles.emptyText]}>
-                You have not blocked any accounts yet. To block an account, go
-                to their profile and selected "Block account" from the menu on
-                their account.
-              </Text>
-            </View>
+            {isError ? (
+              <ErrorScreen
+                title="Oops!"
+                message={cleanError(error)}
+                onPressTryAgain={refetch}
+              />
+            ) : (
+              <View style={[styles.empty, pal.viewLight]}>
+                <Text type="lg" style={[pal.text, styles.emptyText]}>
+                  You have not blocked any accounts yet. To block an account, go
+                  to their profile and selected "Block account" from the menu on
+                  their account.
+                </Text>
+              </View>
+            )}
           </View>
         ) : (
           <FlatList
             style={[!isTabletOrDesktop && styles.flex1]}
-            data={blockedAccounts.blocks}
+            data={profiles}
             keyExtractor={(item: ActorDefs.ProfileView) => item.did}
             refreshControl={
               <RefreshControl
-                refreshing={blockedAccounts.isRefreshing}
+                refreshing={isPTRing}
                 onRefresh={onRefresh}
                 tintColor={pal.colors.text}
                 titleColor={pal.colors.text}
@@ -120,20 +151,19 @@ export const ModerationBlockedAccounts = withAuthRequired(
             renderItem={renderItem}
             initialNumToRender={15}
             // FIXME(dan)
-            // eslint-disable-next-line react/no-unstable-nested-components
+
             ListFooterComponent={() => (
               <View style={styles.footer}>
-                {blockedAccounts.isLoading && <ActivityIndicator />}
+                {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
               </View>
             )}
-            extraData={blockedAccounts.isLoading}
             // @ts-ignore our .web version only -prf
             desktopFixedHeight
           />
         )}
       </CenteredView>
     )
-  }),
+  },
 )
 
 const styles = StyleSheet.create({
diff --git a/src/view/screens/ModerationMutedAccounts.tsx b/src/view/screens/ModerationMutedAccounts.tsx
index 2fa27ee54..fe0b4bf14 100644
--- a/src/view/screens/ModerationMutedAccounts.tsx
+++ b/src/view/screens/ModerationMutedAccounts.tsx
@@ -1,4 +1,4 @@
-import React, {useMemo} from 'react'
+import React from 'react'
 import {
   ActivityIndicator,
   FlatList,
@@ -8,53 +8,78 @@ import {
 } from 'react-native'
 import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
 import {Text} from '../com/util/text/Text'
-import {useStores} from 'state/index'
 import {usePalette} from 'lib/hooks/usePalette'
 import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
 import {withAuthRequired} from 'view/com/auth/withAuthRequired'
-import {observer} from 'mobx-react-lite'
 import {NativeStackScreenProps} from '@react-navigation/native-stack'
 import {CommonNavigatorParams} from 'lib/routes/types'
-import {MutedAccountsModel} from 'state/models/lists/muted-accounts'
 import {useAnalytics} from 'lib/analytics/analytics'
 import {useFocusEffect} from '@react-navigation/native'
 import {ViewHeader} from '../com/util/ViewHeader'
 import {CenteredView} from 'view/com/util/Views'
+import {ErrorScreen} from '../com/util/error/ErrorScreen'
 import {ProfileCard} from 'view/com/profile/ProfileCard'
 import {logger} from '#/logger'
 import {useSetMinimalShellMode} from '#/state/shell'
+import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts'
+import {cleanError} from '#/lib/strings/errors'
 
 type Props = NativeStackScreenProps<
   CommonNavigatorParams,
   'ModerationMutedAccounts'
 >
 export const ModerationMutedAccounts = withAuthRequired(
-  observer(function ModerationMutedAccountsImpl({}: Props) {
+  function ModerationMutedAccountsImpl({}: Props) {
     const pal = usePalette('default')
-    const store = useStores()
     const setMinimalShellMode = useSetMinimalShellMode()
     const {isTabletOrDesktop} = useWebMediaQueries()
     const {screen} = useAnalytics()
-    const mutedAccounts = useMemo(() => new MutedAccountsModel(store), [store])
+    const [isPTRing, setIsPTRing] = React.useState(false)
+    const {
+      data,
+      dataUpdatedAt,
+      isFetching,
+      isError,
+      error,
+      refetch,
+      hasNextPage,
+      fetchNextPage,
+      isFetchingNextPage,
+    } = useMyMutedAccountsQuery()
+    const isEmpty = !isFetching && !data?.pages[0]?.mutes.length
+    const profiles = React.useMemo(() => {
+      if (data?.pages) {
+        return data.pages.flatMap(page => page.mutes)
+      }
+      return []
+    }, [data])
 
     useFocusEffect(
       React.useCallback(() => {
         screen('MutedAccounts')
         setMinimalShellMode(false)
-        mutedAccounts.refresh()
-      }, [screen, setMinimalShellMode, mutedAccounts]),
+      }, [screen, setMinimalShellMode]),
     )
 
-    const onRefresh = React.useCallback(() => {
-      mutedAccounts.refresh()
-    }, [mutedAccounts])
-    const onEndReached = React.useCallback(() => {
-      mutedAccounts
-        .loadMore()
-        .catch(err =>
-          logger.error('Failed to load more muted accounts', {error: err}),
-        )
-    }, [mutedAccounts])
+    const onRefresh = React.useCallback(async () => {
+      setIsPTRing(true)
+      try {
+        await refetch()
+      } catch (err) {
+        logger.error('Failed to refresh my muted accounts', {error: err})
+      }
+      setIsPTRing(false)
+    }, [refetch, setIsPTRing])
+
+    const onEndReached = React.useCallback(async () => {
+      if (isFetching || !hasNextPage || isError) return
+
+      try {
+        await fetchNextPage()
+      } catch (err) {
+        logger.error('Failed to load more of my muted accounts', {error: err})
+      }
+    }, [isFetching, hasNextPage, isError, fetchNextPage])
 
     const renderItem = ({
       item,
@@ -67,6 +92,7 @@ export const ModerationMutedAccounts = withAuthRequired(
         testID={`mutedAccount-${index}`}
         key={item.did}
         profile={item}
+        dataUpdatedAt={dataUpdatedAt}
       />
     )
     return (
@@ -89,24 +115,32 @@ export const ModerationMutedAccounts = withAuthRequired(
           Muted accounts have their posts removed from your feed and from your
           notifications. Mutes are completely private.
         </Text>
-        {!mutedAccounts.hasContent ? (
+        {isEmpty ? (
           <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
-            <View style={[styles.empty, pal.viewLight]}>
-              <Text type="lg" style={[pal.text, styles.emptyText]}>
-                You have not muted any accounts yet. To mute an account, go to
-                their profile and selected "Mute account" from the menu on their
-                account.
-              </Text>
-            </View>
+            {isError ? (
+              <ErrorScreen
+                title="Oops!"
+                message={cleanError(error)}
+                onPressTryAgain={refetch}
+              />
+            ) : (
+              <View style={[styles.empty, pal.viewLight]}>
+                <Text type="lg" style={[pal.text, styles.emptyText]}>
+                  You have not muted any accounts yet. To mute an account, go to
+                  their profile and selected "Mute account" from the menu on
+                  their account.
+                </Text>
+              </View>
+            )}
           </View>
         ) : (
           <FlatList
             style={[!isTabletOrDesktop && styles.flex1]}
-            data={mutedAccounts.mutes}
+            data={profiles}
             keyExtractor={item => item.did}
             refreshControl={
               <RefreshControl
-                refreshing={mutedAccounts.isRefreshing}
+                refreshing={isPTRing}
                 onRefresh={onRefresh}
                 tintColor={pal.colors.text}
                 titleColor={pal.colors.text}
@@ -116,20 +150,19 @@ export const ModerationMutedAccounts = withAuthRequired(
             renderItem={renderItem}
             initialNumToRender={15}
             // FIXME(dan)
-            // eslint-disable-next-line react/no-unstable-nested-components
+
             ListFooterComponent={() => (
               <View style={styles.footer}>
-                {mutedAccounts.isLoading && <ActivityIndicator />}
+                {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
               </View>
             )}
-            extraData={mutedAccounts.isLoading}
             // @ts-ignore our .web version only -prf
             desktopFixedHeight
           />
         )}
       </CenteredView>
     )
-  }),
+  },
 )
 
 const styles = StyleSheet.create({