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 screenszio/stable
parent
0501c2be77
commit
a81c4b68fa
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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,
|
||||||
|
})
|
||||||
|
}
|
|
@ -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
|
||||||
|
|
|
@ -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,
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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({
|
||||||
|
|
|
@ -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({
|
||||||
|
|
Loading…
Reference in New Issue