cleanup mod screens (#8199)

This commit is contained in:
Samuel Newman
2025-04-25 00:36:39 +03:00
committed by GitHub
parent 278a54850b
commit 69f656f283
2 changed files with 170 additions and 198 deletions
+86 -100
View File
@@ -1,19 +1,10 @@
import React from 'react' import {useCallback, useMemo, useState} from 'react'
import { import {type StyleProp, View, type ViewStyle} from 'react-native'
ActivityIndicator,
FlatList,
RefreshControl,
StyleSheet,
View,
} from 'react-native'
import {type AppBskyActorDefs as ActorDefs} from '@atproto/api' import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {type CommonNavigatorParams} from '#/lib/routes/types' import {type CommonNavigatorParams} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -21,11 +12,12 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts' import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {Text} from '#/view/com/util/text/Text' import {List} from '#/view/com/util/List'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {ListFooter} from '#/components/Lists'
import * as ProfileCard from '#/components/ProfileCard' import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps< type Props = NativeStackScreenProps<
CommonNavigatorParams, CommonNavigatorParams,
@@ -33,13 +25,10 @@ type Props = NativeStackScreenProps<
> >
export function ModerationBlockedAccounts({}: Props) { export function ModerationBlockedAccounts({}: Props) {
const t = useTheme() const t = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrDesktop} = useWebMediaQueries()
const moderationOpts = useModerationOpts() const moderationOpts = useModerationOpts()
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = useState(false)
const { const {
data, data,
isFetching, isFetching,
@@ -51,7 +40,7 @@ export function ModerationBlockedAccounts({}: Props) {
isFetchingNextPage, isFetchingNextPage,
} = useMyBlockedAccountsQuery() } = useMyBlockedAccountsQuery()
const isEmpty = !isFetching && !data?.pages[0]?.blocks.length const isEmpty = !isFetching && !data?.pages[0]?.blocks.length
const profiles = React.useMemo(() => { const profiles = useMemo(() => {
if (data?.pages) { if (data?.pages) {
return data.pages.flatMap(page => page.blocks) return data.pages.flatMap(page => page.blocks)
} }
@@ -59,12 +48,12 @@ export function ModerationBlockedAccounts({}: Props) {
}, [data]) }, [data])
useFocusEffect( useFocusEffect(
React.useCallback(() => { useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
) )
const onRefresh = React.useCallback(async () => { const onRefresh = useCallback(async () => {
setIsPTRing(true) setIsPTRing(true)
try { try {
await refetch() await refetch()
@@ -74,7 +63,7 @@ export function ModerationBlockedAccounts({}: Props) {
setIsPTRing(false) setIsPTRing(false)
}, [refetch, setIsPTRing]) }, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => { const onEndReached = useCallback(async () => {
if (isFetching || !hasNextPage || isError) return if (isFetching || !hasNextPage || isError) return
try { try {
@@ -104,28 +93,22 @@ export function ModerationBlockedAccounts({}: Props) {
</View> </View>
) )
} }
return ( return (
<Layout.Screen testID="blockedAccountsScreen"> <Layout.Screen testID="blockedAccountsScreen">
<Layout.Center style={[a.flex_1, {paddingBottom: 100}]}> <Layout.Center>
<ViewHeader title={_(msg`Blocked Accounts`)} showOnDesktop /> <Layout.Header.Outer>
<Text <Layout.Header.BackButton />
type="sm" <Layout.Header.Content>
style={[ <Layout.Header.TitleText>
styles.description, <Trans>Blocked Accounts</Trans>
pal.text, </Layout.Header.TitleText>
isTabletOrDesktop && styles.descriptionDesktop, </Layout.Header.Content>
{ <Layout.Header.Slot />
marginTop: 20, </Layout.Header.Outer>
},
]}>
<Trans>
Blocked accounts cannot reply in your threads, mention you, or
otherwise interact with you. You will not see their content and they
will be prevented from seeing yours.
</Trans>
</Text>
{isEmpty ? ( {isEmpty ? (
<View style={[pal.border]}> <View>
<Info style={[a.border_b]} />
{isError ? ( {isError ? (
<ErrorScreen <ErrorScreen
title="Oops!" title="Oops!"
@@ -133,42 +116,29 @@ export function ModerationBlockedAccounts({}: Props) {
onPressTryAgain={refetch} onPressTryAgain={refetch}
/> />
) : ( ) : (
<View style={[styles.empty, pal.viewLight]}> <Empty />
<Text type="lg" style={[pal.text, styles.emptyText]}>
<Trans>
You have not blocked any accounts yet. To block an account,
go to their profile and select "Block account" from the menu
on their account.
</Trans>
</Text>
</View>
)} )}
</View> </View>
) : ( ) : (
<FlatList <List
style={[!isTabletOrDesktop && styles.flex1]}
data={profiles} data={profiles}
keyExtractor={(item: ActorDefs.ProfileView) => item.did} keyExtractor={(item: ActorDefs.ProfileView) => item.did}
refreshControl={ refreshing={isPTRing}
<RefreshControl onRefresh={onRefresh}
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
// FIXME(dan) // FIXME(dan)
ListFooterComponent={() => ( ListHeaderComponent={Info}
<View style={styles.footer}> ListFooterComponent={
{(isFetching || isFetchingNextPage) && <ActivityIndicator />} <ListFooter
</View> isFetchingNextPage={isFetchingNextPage}
)} hasNextPage={hasNextPage}
// @ts-ignore our .web version only -prf error={cleanError(error)}
desktopFixedHeight onRetry={fetchNextPage}
/>
}
/> />
)} )}
</Layout.Center> </Layout.Center>
@@ -176,37 +146,53 @@ export function ModerationBlockedAccounts({}: Props) {
) )
} }
const styles = StyleSheet.create({ function Empty() {
title: { const t = useTheme()
textAlign: 'center', return (
marginTop: 12, <View style={[a.pt_2xl, a.px_xl, a.align_center]}>
marginBottom: 12, <View
}, style={[
description: { a.py_md,
textAlign: 'center', a.px_lg,
paddingHorizontal: 30, a.rounded_sm,
marginBottom: 14, t.atoms.bg_contrast_25,
}, a.border,
descriptionDesktop: { t.atoms.border_contrast_low,
marginTop: 14, {maxWidth: 400},
}, ]}>
<Text style={[a.text_sm, a.text_center, t.atoms.text_contrast_high]}>
<Trans>
You have not blocked any accounts yet. To block an account, go to
their profile and select "Block account" from the menu on their
account.
</Trans>
</Text>
</View>
</View>
)
}
flex1: { function Info({style}: {style?: StyleProp<ViewStyle>}) {
flex: 1, const t = useTheme()
}, return (
empty: { <View
paddingHorizontal: 20, style={[
paddingVertical: 20, a.w_full,
borderRadius: 16, t.atoms.bg_contrast_25,
marginHorizontal: 24, a.py_md,
marginTop: 10, a.px_xl,
}, a.border_t,
emptyText: { {marginTop: a.border.borderWidth * -1},
textAlign: 'center', t.atoms.border_contrast_low,
}, style,
]}>
footer: { <Text style={[a.text_center, a.text_sm, t.atoms.text_contrast_high]}>
height: 200, <Trans>
paddingTop: 20, Blocked accounts cannot reply in your threads, mention you, or
}, otherwise interact with you. You will not see their content and they
}) will be prevented from seeing yours.
</Trans>
</Text>
</View>
)
}
+84 -98
View File
@@ -1,19 +1,11 @@
import React from 'react' import {useCallback, useMemo, useState} from 'react'
import { import {type StyleProp, View, type ViewStyle} from 'react-native'
ActivityIndicator,
FlatList,
RefreshControl,
StyleSheet,
View,
} from 'react-native'
import {type AppBskyActorDefs as ActorDefs} from '@atproto/api' import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native' import {useFocusEffect} from '@react-navigation/native'
import {type NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
import {usePalette} from '#/lib/hooks/usePalette'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {type CommonNavigatorParams} from '#/lib/routes/types' import {type CommonNavigatorParams} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
@@ -21,11 +13,12 @@ import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts' import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts'
import {useSetMinimalShellMode} from '#/state/shell' import {useSetMinimalShellMode} from '#/state/shell'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen' import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {Text} from '#/view/com/util/text/Text' import {List} from '#/view/com/util/List'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {atoms as a, useTheme} from '#/alf' import {atoms as a, useTheme} from '#/alf'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import {ListFooter} from '#/components/Lists'
import * as ProfileCard from '#/components/ProfileCard' import * as ProfileCard from '#/components/ProfileCard'
import {Text} from '#/components/Typography'
type Props = NativeStackScreenProps< type Props = NativeStackScreenProps<
CommonNavigatorParams, CommonNavigatorParams,
@@ -33,13 +26,11 @@ type Props = NativeStackScreenProps<
> >
export function ModerationMutedAccounts({}: Props) { export function ModerationMutedAccounts({}: Props) {
const t = useTheme() const t = useTheme()
const pal = usePalette('default') const moderationOpts = useModerationOpts()
const {_} = useLingui() const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrDesktop} = useWebMediaQueries()
const moderationOpts = useModerationOpts()
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = useState(false)
const { const {
data, data,
isFetching, isFetching,
@@ -51,7 +42,7 @@ export function ModerationMutedAccounts({}: Props) {
isFetchingNextPage, isFetchingNextPage,
} = useMyMutedAccountsQuery() } = useMyMutedAccountsQuery()
const isEmpty = !isFetching && !data?.pages[0]?.mutes.length const isEmpty = !isFetching && !data?.pages[0]?.mutes.length
const profiles = React.useMemo(() => { const profiles = useMemo(() => {
if (data?.pages) { if (data?.pages) {
return data.pages.flatMap(page => page.mutes) return data.pages.flatMap(page => page.mutes)
} }
@@ -59,12 +50,12 @@ export function ModerationMutedAccounts({}: Props) {
}, [data]) }, [data])
useFocusEffect( useFocusEffect(
React.useCallback(() => { useCallback(() => {
setMinimalShellMode(false) setMinimalShellMode(false)
}, [setMinimalShellMode]), }, [setMinimalShellMode]),
) )
const onRefresh = React.useCallback(async () => { const onRefresh = useCallback(async () => {
setIsPTRing(true) setIsPTRing(true)
try { try {
await refetch() await refetch()
@@ -74,7 +65,7 @@ export function ModerationMutedAccounts({}: Props) {
setIsPTRing(false) setIsPTRing(false)
}, [refetch, setIsPTRing]) }, [refetch, setIsPTRing])
const onEndReached = React.useCallback(async () => { const onEndReached = useCallback(async () => {
if (isFetching || !hasNextPage || isError) return if (isFetching || !hasNextPage || isError) return
try { try {
@@ -120,25 +111,19 @@ export function ModerationMutedAccounts({}: Props) {
} }
return ( return (
<Layout.Screen testID="mutedAccountsScreen"> <Layout.Screen testID="mutedAccountsScreen">
<ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop /> <Layout.Header.Outer>
<Layout.Center style={[a.flex_1, {paddingBottom: 100}]}> <Layout.Header.BackButton />
<Text <Layout.Header.Content>
type="sm" <Layout.Header.TitleText>
style={[ <Trans>Muted Accounts</Trans>
styles.description, </Layout.Header.TitleText>
pal.text, </Layout.Header.Content>
isTabletOrDesktop && styles.descriptionDesktop, <Layout.Header.Slot />
{ </Layout.Header.Outer>
marginTop: 20, <Layout.Center>
},
]}>
<Trans>
Muted accounts have their posts removed from your feed and from your
notifications. Mutes are completely private.
</Trans>
</Text>
{isEmpty ? ( {isEmpty ? (
<View style={[pal.border]}> <View>
<Info style={[a.border_b]} />
{isError ? ( {isError ? (
<ErrorScreen <ErrorScreen
title="Oops!" title="Oops!"
@@ -146,42 +131,29 @@ export function ModerationMutedAccounts({}: Props) {
onPressTryAgain={refetch} onPressTryAgain={refetch}
/> />
) : ( ) : (
<View style={[styles.empty, pal.viewLight]}> <Empty />
<Text type="lg" style={[pal.text, styles.emptyText]}>
<Trans>
You have not muted any accounts yet. To mute an account, go
to their profile and select "Mute account" from the menu on
their account.
</Trans>
</Text>
</View>
)} )}
</View> </View>
) : ( ) : (
<FlatList <List
style={[!isTabletOrDesktop && styles.flex1]}
data={profiles} data={profiles}
keyExtractor={item => item.did} keyExtractor={item => item.did}
refreshControl={ refreshing={isPTRing}
<RefreshControl onRefresh={onRefresh}
refreshing={isPTRing}
onRefresh={onRefresh}
tintColor={pal.colors.text}
titleColor={pal.colors.text}
/>
}
onEndReached={onEndReached} onEndReached={onEndReached}
renderItem={renderItem} renderItem={renderItem}
initialNumToRender={15} initialNumToRender={15}
// FIXME(dan) // FIXME(dan)
ListFooterComponent={() => ( ListHeaderComponent={Info}
<View style={styles.footer}> ListFooterComponent={
{(isFetching || isFetchingNextPage) && <ActivityIndicator />} <ListFooter
</View> isFetchingNextPage={isFetchingNextPage}
)} hasNextPage={hasNextPage}
// @ts-ignore our .web version only -prf error={cleanError(error)}
desktopFixedHeight onRetry={fetchNextPage}
/>
}
/> />
)} )}
</Layout.Center> </Layout.Center>
@@ -189,37 +161,51 @@ export function ModerationMutedAccounts({}: Props) {
) )
} }
const styles = StyleSheet.create({ function Empty() {
title: { const t = useTheme()
textAlign: 'center', return (
marginTop: 12, <View style={[a.pt_2xl, a.px_xl, a.align_center]}>
marginBottom: 12, <View
}, style={[
description: { a.py_md,
textAlign: 'center', a.px_lg,
paddingHorizontal: 30, a.rounded_sm,
marginBottom: 14, t.atoms.bg_contrast_25,
}, a.border,
descriptionDesktop: { t.atoms.border_contrast_low,
marginTop: 14, {maxWidth: 400},
}, ]}>
<Text style={[a.text_sm, a.text_center, t.atoms.text_contrast_high]}>
<Trans>
You have not muted any accounts yet. To mute an account, go to their
profile and select "Mute account" from the menu on their account.
</Trans>
</Text>
</View>
</View>
)
}
flex1: { function Info({style}: {style?: StyleProp<ViewStyle>}) {
flex: 1, const t = useTheme()
}, return (
empty: { <View
paddingHorizontal: 20, style={[
paddingVertical: 20, a.w_full,
borderRadius: 16, t.atoms.bg_contrast_25,
marginHorizontal: 24, a.py_md,
marginTop: 10, a.px_xl,
}, a.border_t,
emptyText: { {marginTop: a.border.borderWidth * -1},
textAlign: 'center', t.atoms.border_contrast_low,
}, style,
]}>
footer: { <Text style={[a.text_center, a.text_sm, t.atoms.text_contrast_high]}>
height: 200, <Trans>
paddingTop: 20, Muted accounts have their posts removed from your feed and from your
}, notifications. Mutes are completely private.
}) </Trans>
</Text>
</View>
)
}