Replace old ProfileCard with new (#8195)

* Replace usages of old ProfileCard

* Replace Pills with Labels component

* Replace impl of ProfileCardWithFollowButton

* Remove never-used LikesDialog

* Handle missing mod opts

* Add missing profile hover

* use modern button in listmembers

* remove follow button from muted accounts list

---------

Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
Eric Bailey
2025-04-15 08:13:20 -05:00
committed by GitHub
parent 32c2b69b84
commit f46336e341
8 changed files with 189 additions and 509 deletions
-129
View File
@@ -1,129 +0,0 @@
import {useCallback, useMemo} from 'react'
import {ActivityIndicator, FlatList, View} from 'react-native'
import {AppBskyFeedGetLikes as GetLikes} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useLikedByQuery} from '#/state/queries/post-liked-by'
import {useResolveUriQuery} from '#/state/queries/resolve-uri'
import {ProfileCardWithFollowBtn} from '#/view/com/profile/ProfileCard'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {atoms as a, useTheme} from '#/alf'
import * as Dialog from '#/components/Dialog'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography'
interface LikesDialogProps {
control: Dialog.DialogOuterProps['control']
uri: string
}
export function LikesDialog(props: LikesDialogProps) {
return (
<Dialog.Outer control={props.control}>
<Dialog.Handle />
<LikesDialogInner {...props} />
</Dialog.Outer>
)
}
export function LikesDialogInner({control, uri}: LikesDialogProps) {
const {_} = useLingui()
const t = useTheme()
const {
data: resolvedUri,
error: resolveError,
isFetched: hasFetchedResolvedUri,
} = useResolveUriQuery(uri)
const {
data,
isFetching: isFetchingLikedBy,
isFetched: hasFetchedLikedBy,
isFetchingNextPage,
hasNextPage,
fetchNextPage,
isError,
error: likedByError,
} = useLikedByQuery(resolvedUri?.uri)
const isLoading = !hasFetchedResolvedUri || !hasFetchedLikedBy
const likes = useMemo(() => {
if (data?.pages) {
return data.pages.flatMap(page => page.likes)
}
return []
}, [data])
const onEndReached = useCallback(async () => {
if (isFetchingLikedBy || !hasNextPage || isError) return
try {
await fetchNextPage()
} catch (err) {
logger.error('Failed to load more likes', {message: err})
}
}, [isFetchingLikedBy, hasNextPage, isError, fetchNextPage])
const renderItem = useCallback(
({item}: {item: GetLikes.Like}) => {
return (
<ProfileCardWithFollowBtn
key={item.actor.did}
profile={item.actor}
onPress={() => control.close()}
/>
)
},
[control],
)
return (
<Dialog.Inner label={_(msg`Users that have liked this content or profile`)}>
<Text style={[a.text_2xl, a.font_bold, a.leading_tight, a.pb_lg]}>
<Trans>Liked by</Trans>
</Text>
{isLoading ? (
<View style={{minHeight: 300}}>
<Loader size="xl" />
</View>
) : resolveError || likedByError || !data ? (
<ErrorMessage message={cleanError(resolveError || likedByError)} />
) : likes.length === 0 ? (
<View style={[t.atoms.bg_contrast_50, a.px_md, a.py_xl, a.rounded_md]}>
<Text style={[a.text_center]}>
<Trans>
Nobody has liked this yet. Maybe you should be the first!
</Trans>
</Text>
</View>
) : (
<FlatList
data={likes}
keyExtractor={item => item.actor.did}
onEndReached={onEndReached}
renderItem={renderItem}
initialNumToRender={15}
ListFooterComponent={
<ListFooterComponent isFetching={isFetchingNextPage} />
}
/>
)}
<Dialog.Close />
</Dialog.Inner>
)
}
function ListFooterComponent({isFetching}: {isFetching: boolean}) {
if (isFetching) {
return (
<View style={a.pt_lg}>
<ActivityIndicator />
</View>
)
}
return null
}
+39 -14
View File
@@ -8,15 +8,15 @@ import {
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {getModerationCauseKey} from '#/lib/moderation'
import {type LogEvents} from '#/lib/statsig/statsig'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {ProfileCardPills} from '#/view/com/profile/ProfileCard'
import * as Toast from '#/view/com/util/Toast'
import {UserAvatar} from '#/view/com/util/UserAvatar'
import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar'
import {atoms as a, useTheme} from '#/alf'
import {
Button,
@@ -27,6 +27,7 @@ import {
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {Link as InternalLink, type LinkProps} from '#/components/Link'
import * as Pills from '#/components/Pills'
import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky'
@@ -35,13 +36,15 @@ export function Default({
profile,
moderationOpts,
logContext = 'ProfileCard',
testID,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
testID?: string
}) {
return (
<Link profile={profile}>
<Link testID={testID} profile={profile}>
<Card
profile={profile}
moderationOpts={moderationOpts}
@@ -60,8 +63,6 @@ export function Card({
moderationOpts: ModerationOpts
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
}) {
const moderation = moderateProfile(profile, moderationOpts)
return (
<Outer>
<Header>
@@ -74,10 +75,7 @@ export function Card({
/>
</Header>
<ProfileCardPills
followedBy={Boolean(profile.viewer?.followedBy)}
moderation={moderation}
/>
<Labels profile={profile} moderationOpts={moderationOpts} />
<Description profile={profile} />
</Outer>
@@ -87,7 +85,7 @@ export function Card({
export function Outer({
children,
}: {
children: React.ReactElement | React.ReactElement[]
children: React.ReactNode | React.ReactNode[]
}) {
return <View style={[a.w_full, a.flex_1, a.gap_xs]}>{children}</View>
}
@@ -95,7 +93,7 @@ export function Outer({
export function Header({
children,
}: {
children: React.ReactElement | React.ReactElement[]
children: React.ReactNode | React.ReactNode[]
}) {
return <View style={[a.flex_row, a.align_center, a.gap_sm]}>{children}</View>
}
@@ -137,10 +135,9 @@ export function Avatar({
const moderation = moderateProfile(profile, moderationOpts)
return (
<UserAvatar
<PreviewableUserAvatar
size={40}
avatar={profile.avatar}
type={profile.associated?.labeler ? 'labeler' : 'user'}
profile={profile}
moderation={moderation.ui('avatar')}
/>
)
@@ -415,3 +412,31 @@ export function FollowButtonInner({
</View>
)
}
export function Labels({
profile,
moderationOpts,
}: {
profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts
}) {
const moderation = moderateProfile(profile, moderationOpts)
const modui = moderation.ui('profileList')
const followedBy = profile.viewer?.followedBy
if (!followedBy && !modui.inform && !modui.alert) {
return null
}
return (
<Pills.Row style={[a.pt_xs]}>
{followedBy && <Pills.FollowsYou />}
{modui.alerts.map(alert => (
<Pills.Label key={getModerationCauseKey(alert)} cause={alert} />
))}
{modui.informs.map(inform => (
<Pills.Label key={getModerationCauseKey(inform)} cause={inform} />
))}
</Pills.Row>
)
}
+1 -3
View File
@@ -275,9 +275,7 @@ let SearchScreenUserResults = ({
{results.length ? (
<List
data={results}
renderItem={({item}) => (
<ProfileCardWithFollowBtn profile={item} noBg />
)}
renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} />}
keyExtractor={item => item.did}
desktopFixedHeight
contentContainerStyle={{paddingBottom: 100}}
+52 -33
View File
@@ -1,22 +1,23 @@
import React, {useCallback} from 'react'
import {Dimensions, type StyleProp, View, type ViewStyle} from 'react-native'
import {type AppBskyGraphDefs} from '@atproto/api'
import {msg} from '@lingui/macro'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useModalControls} from '#/state/modals'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useListMembersQuery} from '#/state/queries/list-members'
import {useSession} from '#/state/session'
import {ProfileCard} from '#/view/com/profile/ProfileCard'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage'
import {Button} from '#/view/com/util/forms/Button'
import {List, type ListRef} from '#/view/com/util/List'
import {ProfileCardFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn'
import {atoms as a, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button'
import {ListFooter} from '#/components/Lists'
import * as ProfileCard from '#/components/ProfileCard'
import type * as bsky from '#/types/bsky'
const LOADING_ITEM = {_reactKey: '__loading__'}
@@ -47,11 +48,12 @@ export function ListMembers({
headerOffset?: number
desktopFixedHeightOffset?: number
}) {
const t = useTheme()
const {_} = useLingui()
const [isRefreshing, setIsRefreshing] = React.useState(false)
const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls()
const {currentAccount} = useSession()
const moderationOpts = useModerationOpts()
const {
data,
@@ -131,23 +133,6 @@ export function ListMembers({
// rendering
// =
const renderMemberButton = React.useCallback(
(profile: bsky.profile.AnyProfileView) => {
if (!isOwner) {
return null
}
return (
<Button
testID={`user-${profile.handle}-editBtn`}
type="default"
label={_(msg({message: 'Edit', context: 'action'}))}
onPress={() => onPressEditMembership(profile)}
/>
)
},
[isOwner, onPressEditMembership, _],
)
const renderItem = React.useCallback(
({item}: {item: any}) => {
if (item === EMPTY_ITEM) {
@@ -171,26 +156,60 @@ export function ListMembers({
} else if (item === LOADING_ITEM) {
return <ProfileCardFeedLoadingPlaceholder />
}
const profile = (item as AppBskyGraphDefs.ListItemView).subject
if (!moderationOpts) return null
return (
<ProfileCard
testID={`user-${
(item as AppBskyGraphDefs.ListItemView).subject.handle
}`}
profile={(item as AppBskyGraphDefs.ListItemView).subject}
renderButton={renderMemberButton}
style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}}
noModFilter
/>
<View
style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}>
<ProfileCard.Link profile={profile}>
<ProfileCard.Outer>
<ProfileCard.Header>
<ProfileCard.Avatar
profile={profile}
moderationOpts={moderationOpts}
/>
<ProfileCard.NameAndHandle
profile={profile}
moderationOpts={moderationOpts}
/>
{isOwner && (
<Button
testID={`user-${profile.handle}-editBtn`}
label={_(msg({message: 'Edit', context: 'action'}))}
onPress={() => onPressEditMembership(profile)}
size="small"
variant="solid"
color="secondary">
<ButtonText>
<Trans context="action">Edit</Trans>
</ButtonText>
</Button>
)}
</ProfileCard.Header>
<ProfileCard.Labels
profile={profile}
moderationOpts={moderationOpts}
/>
<ProfileCard.Description profile={profile} />
</ProfileCard.Outer>
</ProfileCard.Link>
</View>
)
},
[
renderMemberButton,
renderEmptyState,
error,
onPressTryAgain,
onPressRetryLoadMore,
isMobile,
moderationOpts,
isOwner,
onPressEditMembership,
_,
t,
],
)
+20 -291
View File
@@ -1,307 +1,36 @@
import React from 'react'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native'
import {
AppBskyActorDefs,
moderateProfile,
ModerationDecision,
} from '@atproto/api'
import {useQueryClient} from '@tanstack/react-query'
import {View} from 'react-native'
import {type AppBskyActorDefs} from '@atproto/api'
import {usePalette} from '#/lib/hooks/usePalette'
import {getModerationCauseKey, isJustAMute} from '#/lib/moderation'
import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles'
import {s} from '#/lib/styles'
import {useProfileShadow} from '#/state/cache/profile-shadow'
import {Shadow} from '#/state/cache/types'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {precacheProfile} from '#/state/queries/profile'
import {useSession} from '#/state/session'
import {atoms as a} from '#/alf'
import {
KnownFollowers,
shouldShowKnownFollowers,
} from '#/components/KnownFollowers'
import * as Pills from '#/components/Pills'
import * as bsky from '#/types/bsky'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {PreviewableUserAvatar} from '../util/UserAvatar'
import {FollowButton} from './FollowButton'
export function ProfileCard({
testID,
profile: profileUnshadowed,
noModFilter,
noBg,
noBorder,
renderButton,
onPress,
style,
showKnownFollowers,
}: {
testID?: string
profile: bsky.profile.AnyProfileView
noModFilter?: boolean
noBg?: boolean
noBorder?: boolean
renderButton?: (
profile: Shadow<bsky.profile.AnyProfileView>,
) => React.ReactNode
onPress?: () => void
style?: StyleProp<ViewStyle>
showKnownFollowers?: boolean
}) {
const queryClient = useQueryClient()
const pal = usePalette('default')
const profile = useProfileShadow(profileUnshadowed)
const moderationOpts = useModerationOpts()
const isLabeler = profile?.associated?.labeler
const onBeforePress = React.useCallback(() => {
onPress?.()
precacheProfile(queryClient, profile)
}, [onPress, profile, queryClient])
if (!moderationOpts) {
return null
}
const moderation = moderateProfile(profile, moderationOpts)
const modui = moderation.ui('profileList')
if (!noModFilter && modui.filter && !isJustAMute(modui)) {
return null
}
const knownFollowersVisible =
showKnownFollowers &&
shouldShowKnownFollowers(profile.viewer?.knownFollowers) &&
moderationOpts
const hasDescription = 'description' in profile
return (
<Link
testID={testID}
style={[
styles.outer,
pal.border,
noBorder && styles.outerNoBorder,
!noBg && pal.view,
style,
]}
href={makeProfileLink(profile)}
title={profile.handle}
asAnchor
onBeforePress={onBeforePress}
anchorNoUnderline>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<PreviewableUserAvatar
size={40}
profile={profile}
moderation={moderation.ui('avatar')}
type={isLabeler ? 'labeler' : 'user'}
/>
</View>
<View style={styles.layoutContent}>
<Text
emoji
type="lg"
style={[s.bold, pal.text, a.self_start]}
numberOfLines={1}
lineHeight={1.2}>
{sanitizeDisplayName(
profile.displayName || sanitizeHandle(profile.handle),
moderation.ui('displayName'),
)}
</Text>
<Text emoji type="md" style={[pal.textLight]} numberOfLines={1}>
{sanitizeHandle(profile.handle, '@')}
</Text>
<ProfileCardPills
followedBy={!!profile.viewer?.followedBy}
moderation={moderation}
/>
{!!profile.viewer?.followedBy && <View style={s.flexRow} />}
</View>
{renderButton && !isLabeler ? (
<View style={styles.layoutButton}>{renderButton(profile)}</View>
) : undefined}
</View>
{hasDescription || knownFollowersVisible ? (
<View style={styles.details}>
{hasDescription && profile.description ? (
<Text emoji style={pal.text} numberOfLines={4}>
{profile.description as string}
</Text>
) : null}
{knownFollowersVisible ? (
<View
style={[
a.flex_row,
a.align_center,
a.gap_sm,
!!hasDescription && a.mt_md,
]}>
<KnownFollowers
minimal
profile={profile}
moderationOpts={moderationOpts}
/>
</View>
) : null}
</View>
) : null}
</Link>
)
}
export function ProfileCardPills({
followedBy,
moderation,
}: {
followedBy: boolean
moderation: ModerationDecision
}) {
const modui = moderation.ui('profileList')
if (!followedBy && !modui.inform && !modui.alert) {
return null
}
return (
<Pills.Row style={[a.pt_xs]}>
{followedBy && <Pills.FollowsYou />}
{modui.alerts.map(alert => (
<Pills.Label key={getModerationCauseKey(alert)} cause={alert} />
))}
{modui.informs.map(inform => (
<Pills.Label key={getModerationCauseKey(inform)} cause={inform} />
))}
</Pills.Row>
)
}
import {atoms as a, useTheme} from '#/alf'
import * as ProfileCard from '#/components/ProfileCard'
export function ProfileCardWithFollowBtn({
profile,
noBg,
noBorder,
onPress,
onFollow,
logContext = 'ProfileCard',
showKnownFollowers,
}: {
profile: AppBskyActorDefs.ProfileView
noBg?: boolean
noBorder?: boolean
onPress?: () => void
onFollow?: () => void
logContext?: 'ProfileCard' | 'StarterPackProfilesList'
showKnownFollowers?: boolean
}) {
const {currentAccount} = useSession()
const isMe = profile.did === currentAccount?.did
const t = useTheme()
const moderationOpts = useModerationOpts()
if (!moderationOpts) return null
return (
<ProfileCard
profile={profile}
noBg={noBg}
noBorder={noBorder}
renderButton={
isMe
? undefined
: profileShadow => (
<FollowButton
profile={profileShadow}
logContext={logContext}
onFollow={onFollow}
/>
)
}
onPress={onPress}
showKnownFollowers={!isMe && showKnownFollowers}
/>
<View
style={[
a.py_md,
a.px_xl,
!noBorder && [a.border_t, t.atoms.border_contrast_low],
]}>
<ProfileCard.Default
profile={profile}
moderationOpts={moderationOpts}
logContext={logContext}
/>
</View>
)
}
const styles = StyleSheet.create({
outer: {
borderTopWidth: StyleSheet.hairlineWidth,
paddingHorizontal: 6,
paddingVertical: 4,
},
outerNoBorder: {
borderTopWidth: 0,
},
layout: {
flexDirection: 'row',
alignItems: 'center',
},
layoutAvi: {
alignSelf: 'flex-start',
width: 54,
paddingLeft: 4,
paddingTop: 10,
},
avi: {
width: 40,
height: 40,
borderRadius: 20,
resizeMode: 'cover',
},
layoutContent: {
flex: 1,
paddingRight: 10,
paddingTop: 10,
paddingBottom: 10,
},
layoutButton: {
paddingRight: 10,
},
details: {
justifyContent: 'center',
paddingLeft: 54,
paddingRight: 10,
paddingBottom: 10,
},
pills: {
alignItems: 'flex-start',
flexDirection: 'row',
flexWrap: 'wrap',
columnGap: 6,
rowGap: 2,
},
pill: {
borderRadius: 4,
paddingHorizontal: 6,
paddingVertical: 2,
justifyContent: 'center',
},
btn: {
paddingVertical: 7,
borderRadius: 50,
marginLeft: 6,
paddingHorizontal: 14,
},
followedBy: {
flexDirection: 'row',
paddingLeft: 54,
paddingRight: 20,
marginBottom: 10,
marginTop: -6,
},
followedByAviContainer: {
width: 24,
height: 36,
},
followedByAvi: {
width: 36,
height: 36,
borderRadius: 18,
padding: 2,
},
followsByDesc: {
flex: 1,
paddingRight: 10,
},
})
+19 -13
View File
@@ -1,29 +1,32 @@
/* eslint-disable no-restricted-imports */
import React from 'react'
import {View} from 'react-native'
import {
AppBskyActorDefs,
AppBskyFeedDefs,
AppBskyFeedPost,
ComAtprotoLabelDefs,
type AppBskyActorDefs,
type AppBskyFeedDefs,
type AppBskyFeedPost,
type ComAtprotoLabelDefs,
interpretLabelValueDefinition,
LabelPreference,
type LabelPreference,
LABELS,
mock,
moderatePost,
moderateProfile,
ModerationBehavior,
ModerationDecision,
ModerationOpts,
type ModerationBehavior,
type ModerationDecision,
type ModerationOpts,
RichText,
} from '@atproto/api'
import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings'
import {CommonNavigatorParams, NativeStackScreenProps} from '#/lib/routes/types'
import {
type CommonNavigatorParams,
type NativeStackScreenProps,
} from '#/lib/routes/types'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {moderationOptsOverrideContext} from '#/state/preferences/moderation-opts'
import {FeedNotification} from '#/state/queries/notifications/types'
import {type FeedNotification} from '#/state/queries/notifications/types'
import {
groupNotifications,
shouldFilterNotif,
@@ -42,12 +45,12 @@ import {
ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
} from '#/components/icons/Chevron'
import * as Layout from '#/components/Layout'
import * as ProfileCard from '#/components/ProfileCard'
import {H1, H3, P, Text} from '#/components/Typography'
import {ScreenHider} from '../../components/moderation/ScreenHider'
import {NotificationFeedItem} from '../com/notifications/NotificationFeedItem'
import {PostThreadItem} from '../com/post-thread/PostThreadItem'
import {PostFeedItem} from '../com/posts/PostFeedItem'
import {ProfileCard} from '../com/profile/ProfileCard'
const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
LABELS,
@@ -890,6 +893,9 @@ function MockAccountCard({
moderation: ModerationDecision
}) {
const t = useTheme()
const moderationOpts = useModerationOpts()
if (!moderationOpts) return null
if (moderation.ui('profileList').filter) {
return (
@@ -899,7 +905,7 @@ function MockAccountCard({
)
}
return <ProfileCard profile={profile} />
return <ProfileCard.Card profile={profile} moderationOpts={moderationOpts} />
}
function MockAccountScreen({
+22 -13
View File
@@ -6,35 +6,38 @@ import {
StyleSheet,
View,
} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {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 {CommonNavigatorParams} from '#/lib/routes/types'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useMyBlockedAccountsQuery} from '#/state/queries/my-blocked-accounts'
import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileCard} from '#/view/com/profile/ProfileCard'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {Text} from '#/view/com/util/text/Text'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {atoms as a} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import * as Layout from '#/components/Layout'
import * as ProfileCard from '#/components/ProfileCard'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
'ModerationBlockedAccounts'
>
export function ModerationBlockedAccounts({}: Props) {
const t = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrDesktop} = useWebMediaQueries()
const moderationOpts = useModerationOpts()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
@@ -87,14 +90,20 @@ export function ModerationBlockedAccounts({}: Props) {
}: {
item: ActorDefs.ProfileView
index: number
}) => (
<ProfileCard
testID={`blockedAccount-${index}`}
key={item.did}
profile={item}
noModFilter
/>
)
}) => {
if (!moderationOpts) return null
return (
<View
style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}
key={item.did}>
<ProfileCard.Default
testID={`blockedAccount-${index}`}
profile={item}
moderationOpts={moderationOpts}
/>
</View>
)
}
return (
<Layout.Screen testID="blockedAccountsScreen">
<Layout.Center style={[a.flex_1, {paddingBottom: 100}]}>
+36 -13
View File
@@ -6,35 +6,38 @@ import {
StyleSheet,
View,
} from 'react-native'
import {AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {type AppBskyActorDefs as ActorDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useFocusEffect} from '@react-navigation/native'
import {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 {CommonNavigatorParams} from '#/lib/routes/types'
import {type CommonNavigatorParams} from '#/lib/routes/types'
import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useMyMutedAccountsQuery} from '#/state/queries/my-muted-accounts'
import {useSetMinimalShellMode} from '#/state/shell'
import {ProfileCard} from '#/view/com/profile/ProfileCard'
import {ErrorScreen} from '#/view/com/util/error/ErrorScreen'
import {Text} from '#/view/com/util/text/Text'
import {ViewHeader} from '#/view/com/util/ViewHeader'
import {atoms as a} from '#/alf'
import {atoms as a, useTheme} from '#/alf'
import * as Layout from '#/components/Layout'
import * as ProfileCard from '#/components/ProfileCard'
type Props = NativeStackScreenProps<
CommonNavigatorParams,
'ModerationMutedAccounts'
>
export function ModerationMutedAccounts({}: Props) {
const t = useTheme()
const pal = usePalette('default')
const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrDesktop} = useWebMediaQueries()
const moderationOpts = useModerationOpts()
const [isPTRing, setIsPTRing] = React.useState(false)
const {
@@ -87,14 +90,34 @@ export function ModerationMutedAccounts({}: Props) {
}: {
item: ActorDefs.ProfileView
index: number
}) => (
<ProfileCard
testID={`mutedAccount-${index}`}
key={item.did}
profile={item}
noModFilter
/>
)
}) => {
if (!moderationOpts) return null
return (
<View
style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}
key={item.did}>
<ProfileCard.Link profile={item} testID={`mutedAccount-${index}`}>
<ProfileCard.Outer>
<ProfileCard.Header>
<ProfileCard.Avatar
profile={item}
moderationOpts={moderationOpts}
/>
<ProfileCard.NameAndHandle
profile={item}
moderationOpts={moderationOpts}
/>
</ProfileCard.Header>
<ProfileCard.Labels
profile={item}
moderationOpts={moderationOpts}
/>
<ProfileCard.Description profile={item} />
</ProfileCard.Outer>
</ProfileCard.Link>
</View>
)
}
return (
<Layout.Screen testID="mutedAccountsScreen">
<ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />