Update Muted and Blocked accounts screens (react-query refactor) (#1892)

* Add my-blocked-accounts and my-muted-accounts queries

* Update ProfileCard to use the profile shadow cache and useModerationOpts

* Update blocked accounts and muted accounts screens
zio/stable
Paul Frazee 2023-11-13 17:30:56 -08:00 committed by GitHub
parent 0501c2be77
commit a81c4b68fa
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 212 additions and 289 deletions

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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,
})
}

View File

@ -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,
})
}

View File

@ -11,6 +11,8 @@ import {useSession} from '../session'
import {updateProfileShadow} from '../cache/profile-shadow' import {updateProfileShadow} from '../cache/profile-shadow'
import {uploadBlob} from '#/lib/api' import {uploadBlob} from '#/lib/api'
import {until} from '#/lib/async/until' 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] export const RQKEY = (did: string) => ['profile', did]
@ -147,6 +149,7 @@ export function useProfileUnfollowMutation() {
export function useProfileMuteMutation() { export function useProfileMuteMutation() {
const {agent} = useSession() const {agent} = useSession()
const queryClient = useQueryClient()
return useMutation<void, Error, {did: string}>({ return useMutation<void, Error, {did: string}>({
mutationFn: async ({did}) => { mutationFn: async ({did}) => {
await agent.mute(did) await agent.mute(did)
@ -157,6 +160,9 @@ export function useProfileMuteMutation() {
muted: true, muted: true,
}) })
}, },
onSuccess() {
queryClient.invalidateQueries({queryKey: RQKEY_MY_MUTED()})
},
onError(error, variables) { onError(error, variables) {
// revert the optimistic update // revert the optimistic update
updateProfileShadow(variables.did, { updateProfileShadow(variables.did, {
@ -189,6 +195,7 @@ export function useProfileUnmuteMutation() {
export function useProfileBlockMutation() { export function useProfileBlockMutation() {
const {agent, currentAccount} = useSession() const {agent, currentAccount} = useSession()
const queryClient = useQueryClient()
return useMutation<{uri: string; cid: string}, Error, {did: string}>({ return useMutation<{uri: string; cid: string}, Error, {did: string}>({
mutationFn: async ({did}) => { mutationFn: async ({did}) => {
if (!currentAccount) { if (!currentAccount) {
@ -210,6 +217,7 @@ export function useProfileBlockMutation() {
updateProfileShadow(variables.did, { updateProfileShadow(variables.did, {
blockingUri: data.uri, blockingUri: data.uri,
}) })
queryClient.invalidateQueries({queryKey: RQKEY_MY_BLOCKED()})
}, },
onError(error, variables) { onError(error, variables) {
// revert the optimistic update // revert the optimistic update

View File

@ -64,6 +64,7 @@ export function ListMembers({
const { const {
data, data,
dataUpdatedAt,
isFetching, isFetching,
isFetched, isFetched,
isError, isError,
@ -184,6 +185,7 @@ export function ListMembers({
(item as AppBskyGraphDefs.ListItemView).subject.handle (item as AppBskyGraphDefs.ListItemView).subject.handle
}`} }`}
profile={(item as AppBskyGraphDefs.ListItemView).subject} profile={(item as AppBskyGraphDefs.ListItemView).subject}
dataUpdatedAt={dataUpdatedAt}
renderButton={renderMemberButton} renderButton={renderMemberButton}
style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}} style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
/> />
@ -196,6 +198,7 @@ export function ListMembers({
onPressTryAgain, onPressTryAgain,
onPressRetryLoadMore, onPressRetryLoadMore,
isMobile, isMobile,
dataUpdatedAt,
], ],
) )

View File

@ -21,10 +21,13 @@ import {
getProfileModerationCauses, getProfileModerationCauses,
getModerationCauseKey, getModerationCauseKey,
} from 'lib/moderation' } 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, testID,
profile, profile: profileUnshadowed,
dataUpdatedAt,
noBg, noBg,
noBorder, noBorder,
followers, followers,
@ -33,16 +36,20 @@ export const ProfileCard = observer(function ProfileCardImpl({
}: { }: {
testID?: string testID?: string
profile: AppBskyActorDefs.ProfileViewBasic profile: AppBskyActorDefs.ProfileViewBasic
dataUpdatedAt: number
noBg?: boolean noBg?: boolean
noBorder?: boolean noBorder?: boolean
followers?: AppBskyActorDefs.ProfileView[] | undefined followers?: AppBskyActorDefs.ProfileView[] | undefined
renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => React.ReactNode renderButton?: (profile: AppBskyActorDefs.ProfileViewBasic) => React.ReactNode
style?: StyleProp<ViewStyle> style?: StyleProp<ViewStyle>
}) { }) {
const store = useStores()
const pal = usePalette('default') const pal = usePalette('default')
const profile = useProfileShadow(profileUnshadowed, dataUpdatedAt)
const moderation = moderateProfile(profile, store.preferences.moderationOpts) const moderationOpts = useModerationOpts()
if (!moderationOpts) {
return null
}
const moderation = moderateProfile(profile, moderationOpts)
return ( return (
<Link <Link
@ -100,7 +107,7 @@ export const ProfileCard = observer(function ProfileCardImpl({
<FollowersList followers={followers} /> <FollowersList followers={followers} />
</Link> </Link>
) )
}) }
function ProfileCardPills({ function ProfileCardPills({
followedBy, followedBy,

View File

@ -1,4 +1,4 @@
import React, {useMemo} from 'react' import React from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
@ -8,56 +8,78 @@ import {
} from 'react-native' } from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {Text} from '../com/util/text/Text' import {Text} from '../com/util/text/Text'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {observer} from 'mobx-react-lite'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from 'lib/routes/types' import {CommonNavigatorParams} from 'lib/routes/types'
import {BlockedAccountsModel} from 'state/models/lists/blocked-accounts'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {ProfileCard} from 'view/com/profile/ProfileCard' import {ProfileCard} from 'view/com/profile/ProfileCard'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts'
import {cleanError} from '#/lib/strings/errors'
type Props = NativeStackScreenProps< type Props = NativeStackScreenProps<
CommonNavigatorParams, CommonNavigatorParams,
'ModerationBlockedAccounts' 'ModerationBlockedAccounts'
> >
export const ModerationBlockedAccounts = withAuthRequired( export const ModerationBlockedAccounts = withAuthRequired(
observer(function ModerationBlockedAccountsImpl({}: Props) { function ModerationBlockedAccountsImpl({}: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const {screen} = useAnalytics() const {screen} = useAnalytics()
const blockedAccounts = useMemo( const [isPTRing, setIsPTRing] = React.useState(false)
() => new BlockedAccountsModel(store), const {
[store], 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( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
screen('BlockedAccounts') screen('BlockedAccounts')
setMinimalShellMode(false) setMinimalShellMode(false)
blockedAccounts.refresh() }, [screen, setMinimalShellMode]),
}, [screen, setMinimalShellMode, blockedAccounts]),
) )
const onRefresh = React.useCallback(() => { const onRefresh = React.useCallback(async () => {
blockedAccounts.refresh() setIsPTRing(true)
}, [blockedAccounts]) try {
const onEndReached = React.useCallback(() => { await refetch()
blockedAccounts } catch (err) {
.loadMore() logger.error('Failed to refresh my muted accounts', {error: err})
.catch(err => }
logger.error('Failed to load more blocked accounts', {error: err}), setIsPTRing(false)
) }, [refetch, setIsPTRing])
}, [blockedAccounts])
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 = ({ const renderItem = ({
item, item,
@ -70,6 +92,7 @@ export const ModerationBlockedAccounts = withAuthRequired(
testID={`blockedAccount-${index}`} testID={`blockedAccount-${index}`}
key={item.did} key={item.did}
profile={item} profile={item}
dataUpdatedAt={dataUpdatedAt}
/> />
) )
return ( return (
@ -93,8 +116,15 @@ export const ModerationBlockedAccounts = withAuthRequired(
otherwise interact with you. You will not see their content and they otherwise interact with you. You will not see their content and they
will be prevented from seeing yours. will be prevented from seeing yours.
</Text> </Text>
{!blockedAccounts.hasContent ? ( {isEmpty ? (
<View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
{isError ? (
<ErrorScreen
title="Oops!"
message={cleanError(error)}
onPressTryAgain={refetch}
/>
) : (
<View style={[styles.empty, pal.viewLight]}> <View style={[styles.empty, pal.viewLight]}>
<Text type="lg" style={[pal.text, styles.emptyText]}> <Text type="lg" style={[pal.text, styles.emptyText]}>
You have not blocked any accounts yet. To block an account, go You have not blocked any accounts yet. To block an account, go
@ -102,15 +132,16 @@ export const ModerationBlockedAccounts = withAuthRequired(
their account. their account.
</Text> </Text>
</View> </View>
)}
</View> </View>
) : ( ) : (
<FlatList <FlatList
style={[!isTabletOrDesktop && styles.flex1]} style={[!isTabletOrDesktop && styles.flex1]}
data={blockedAccounts.blocks} data={profiles}
keyExtractor={(item: ActorDefs.ProfileView) => item.did} keyExtractor={(item: ActorDefs.ProfileView) => item.did}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={blockedAccounts.isRefreshing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text} tintColor={pal.colors.text}
titleColor={pal.colors.text} titleColor={pal.colors.text}
@ -120,20 +151,19 @@ export const ModerationBlockedAccounts = withAuthRequired(
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
// FIXME(dan) // FIXME(dan)
// eslint-disable-next-line react/no-unstable-nested-components
ListFooterComponent={() => ( ListFooterComponent={() => (
<View style={styles.footer}> <View style={styles.footer}>
{blockedAccounts.isLoading && <ActivityIndicator />} {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
</View> </View>
)} )}
extraData={blockedAccounts.isLoading}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
/> />
)} )}
</CenteredView> </CenteredView>
) )
}), },
) )
const styles = StyleSheet.create({ const styles = StyleSheet.create({

View File

@ -1,4 +1,4 @@
import React, {useMemo} from 'react' import React from 'react'
import { import {
ActivityIndicator, ActivityIndicator,
FlatList, FlatList,
@ -8,53 +8,78 @@ import {
} from 'react-native' } from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api' import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {Text} from '../com/util/text/Text' import {Text} from '../com/util/text/Text'
import {useStores} from 'state/index'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries' import {useWebMediaQueries} from 'lib/hooks/useWebMediaQueries'
import {withAuthRequired} from 'view/com/auth/withAuthRequired' import {withAuthRequired} from 'view/com/auth/withAuthRequired'
import {observer} from 'mobx-react-lite'
import {NativeStackScreenProps} from '@react-navigation/native-stack' import {NativeStackScreenProps} from '@react-navigation/native-stack'
import {CommonNavigatorParams} from 'lib/routes/types' import {CommonNavigatorParams} from 'lib/routes/types'
import {MutedAccountsModel} from 'state/models/lists/muted-accounts'
import {useAnalytics} from 'lib/analytics/analytics' import {useAnalytics} from 'lib/analytics/analytics'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {ViewHeader} from '../com/util/ViewHeader' import {ViewHeader} from '../com/util/ViewHeader'
import {CenteredView} from 'view/com/util/Views' import {CenteredView} from 'view/com/util/Views'
import {ErrorScreen} from '../com/util/error/ErrorScreen'
import {ProfileCard} from 'view/com/profile/ProfileCard' import {ProfileCard} from 'view/com/profile/ProfileCard'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts'
import {cleanError} from '#/lib/strings/errors'
type Props = NativeStackScreenProps< type Props = NativeStackScreenProps<
CommonNavigatorParams, CommonNavigatorParams,
'ModerationMutedAccounts' 'ModerationMutedAccounts'
> >
export const ModerationMutedAccounts = withAuthRequired( export const ModerationMutedAccounts = withAuthRequired(
observer(function ModerationMutedAccountsImpl({}: Props) { function ModerationMutedAccountsImpl({}: Props) {
const pal = usePalette('default') const pal = usePalette('default')
const store = useStores()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const {screen} = useAnalytics() 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( useFocusEffect(
React.useCallback(() => { React.useCallback(() => {
screen('MutedAccounts') screen('MutedAccounts')
setMinimalShellMode(false) setMinimalShellMode(false)
mutedAccounts.refresh() }, [screen, setMinimalShellMode]),
}, [screen, setMinimalShellMode, mutedAccounts]),
) )
const onRefresh = React.useCallback(() => { const onRefresh = React.useCallback(async () => {
mutedAccounts.refresh() setIsPTRing(true)
}, [mutedAccounts]) try {
const onEndReached = React.useCallback(() => { await refetch()
mutedAccounts } catch (err) {
.loadMore() logger.error('Failed to refresh my muted accounts', {error: err})
.catch(err => }
logger.error('Failed to load more muted accounts', {error: err}), setIsPTRing(false)
) }, [refetch, setIsPTRing])
}, [mutedAccounts])
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 = ({ const renderItem = ({
item, item,
@ -67,6 +92,7 @@ export const ModerationMutedAccounts = withAuthRequired(
testID={`mutedAccount-${index}`} testID={`mutedAccount-${index}`}
key={item.did} key={item.did}
profile={item} profile={item}
dataUpdatedAt={dataUpdatedAt}
/> />
) )
return ( return (
@ -89,24 +115,32 @@ export const ModerationMutedAccounts = withAuthRequired(
Muted accounts have their posts removed from your feed and from your Muted accounts have their posts removed from your feed and from your
notifications. Mutes are completely private. notifications. Mutes are completely private.
</Text> </Text>
{!mutedAccounts.hasContent ? ( {isEmpty ? (
<View style={[pal.border, !isTabletOrDesktop && styles.flex1]}> <View style={[pal.border, !isTabletOrDesktop && styles.flex1]}>
{isError ? (
<ErrorScreen
title="Oops!"
message={cleanError(error)}
onPressTryAgain={refetch}
/>
) : (
<View style={[styles.empty, pal.viewLight]}> <View style={[styles.empty, pal.viewLight]}>
<Text type="lg" style={[pal.text, styles.emptyText]}> <Text type="lg" style={[pal.text, styles.emptyText]}>
You have not muted any accounts yet. To mute an account, go to You have not muted any accounts yet. To mute an account, go to
their profile and selected "Mute account" from the menu on their their profile and selected "Mute account" from the menu on
account. their account.
</Text> </Text>
</View> </View>
)}
</View> </View>
) : ( ) : (
<FlatList <FlatList
style={[!isTabletOrDesktop && styles.flex1]} style={[!isTabletOrDesktop && styles.flex1]}
data={mutedAccounts.mutes} data={profiles}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshControl={ refreshControl={
<RefreshControl <RefreshControl
refreshing={mutedAccounts.isRefreshing} refreshing={isPTRing}
onRefresh={onRefresh} onRefresh={onRefresh}
tintColor={pal.colors.text} tintColor={pal.colors.text}
titleColor={pal.colors.text} titleColor={pal.colors.text}
@ -116,20 +150,19 @@ export const ModerationMutedAccounts = withAuthRequired(
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
// FIXME(dan) // FIXME(dan)
// eslint-disable-next-line react/no-unstable-nested-components
ListFooterComponent={() => ( ListFooterComponent={() => (
<View style={styles.footer}> <View style={styles.footer}>
{mutedAccounts.isLoading && <ActivityIndicator />} {(isFetching || isFetchingNextPage) && <ActivityIndicator />}
</View> </View>
)} )}
extraData={mutedAccounts.isLoading}
// @ts-ignore our .web version only -prf // @ts-ignore our .web version only -prf
desktopFixedHeight desktopFixedHeight
/> />
)} )}
</CenteredView> </CenteredView>
) )
}), },
) )
const styles = StyleSheet.create({ const styles = StyleSheet.create({