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 {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {getModerationCauseKey} from '#/lib/moderation'
import {type LogEvents} from '#/lib/statsig/statsig' import {type LogEvents} from '#/lib/statsig/statsig'
import {sanitizeDisplayName} from '#/lib/strings/display-names' import {sanitizeDisplayName} from '#/lib/strings/display-names'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {useProfileShadow} from '#/state/cache/profile-shadow' import {useProfileShadow} from '#/state/cache/profile-shadow'
import {useProfileFollowMutationQueue} from '#/state/queries/profile' import {useProfileFollowMutationQueue} from '#/state/queries/profile'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {ProfileCardPills} from '#/view/com/profile/ProfileCard'
import * as Toast from '#/view/com/util/Toast' 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 {atoms as a, useTheme} from '#/alf'
import { import {
Button, Button,
@@ -27,6 +27,7 @@ import {
import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check' import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus' import {PlusLarge_Stroke2_Corner0_Rounded as Plus} from '#/components/icons/Plus'
import {Link as InternalLink, type LinkProps} from '#/components/Link' import {Link as InternalLink, type LinkProps} from '#/components/Link'
import * as Pills from '#/components/Pills'
import {RichText} from '#/components/RichText' import {RichText} from '#/components/RichText'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
@@ -35,13 +36,15 @@ export function Default({
profile, profile,
moderationOpts, moderationOpts,
logContext = 'ProfileCard', logContext = 'ProfileCard',
testID,
}: { }: {
profile: bsky.profile.AnyProfileView profile: bsky.profile.AnyProfileView
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
logContext?: 'ProfileCard' | 'StarterPackProfilesList' logContext?: 'ProfileCard' | 'StarterPackProfilesList'
testID?: string
}) { }) {
return ( return (
<Link profile={profile}> <Link testID={testID} profile={profile}>
<Card <Card
profile={profile} profile={profile}
moderationOpts={moderationOpts} moderationOpts={moderationOpts}
@@ -60,8 +63,6 @@ export function Card({
moderationOpts: ModerationOpts moderationOpts: ModerationOpts
logContext?: 'ProfileCard' | 'StarterPackProfilesList' logContext?: 'ProfileCard' | 'StarterPackProfilesList'
}) { }) {
const moderation = moderateProfile(profile, moderationOpts)
return ( return (
<Outer> <Outer>
<Header> <Header>
@@ -74,10 +75,7 @@ export function Card({
/> />
</Header> </Header>
<ProfileCardPills <Labels profile={profile} moderationOpts={moderationOpts} />
followedBy={Boolean(profile.viewer?.followedBy)}
moderation={moderation}
/>
<Description profile={profile} /> <Description profile={profile} />
</Outer> </Outer>
@@ -87,7 +85,7 @@ export function Card({
export function Outer({ export function Outer({
children, 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> return <View style={[a.w_full, a.flex_1, a.gap_xs]}>{children}</View>
} }
@@ -95,7 +93,7 @@ export function Outer({
export function Header({ export function Header({
children, 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> 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) const moderation = moderateProfile(profile, moderationOpts)
return ( return (
<UserAvatar <PreviewableUserAvatar
size={40} size={40}
avatar={profile.avatar} profile={profile}
type={profile.associated?.labeler ? 'labeler' : 'user'}
moderation={moderation.ui('avatar')} moderation={moderation.ui('avatar')}
/> />
) )
@@ -415,3 +412,31 @@ export function FollowButtonInner({
</View> </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 ? ( {results.length ? (
<List <List
data={results} data={results}
renderItem={({item}) => ( renderItem={({item}) => <ProfileCardWithFollowBtn profile={item} />}
<ProfileCardWithFollowBtn profile={item} noBg />
)}
keyExtractor={item => item.did} keyExtractor={item => item.did}
desktopFixedHeight desktopFixedHeight
contentContainerStyle={{paddingBottom: 100}} contentContainerStyle={{paddingBottom: 100}}
+52 -33
View File
@@ -1,22 +1,23 @@
import React, {useCallback} from 'react' import React, {useCallback} from 'react'
import {Dimensions, type StyleProp, View, type ViewStyle} from 'react-native' import {Dimensions, type StyleProp, View, type ViewStyle} from 'react-native'
import {type AppBskyGraphDefs} from '@atproto/api' import {type AppBskyGraphDefs} from '@atproto/api'
import {msg} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useWebMediaQueries} from '#/lib/hooks/useWebMediaQueries'
import {cleanError} from '#/lib/strings/errors' import {cleanError} from '#/lib/strings/errors'
import {logger} from '#/logger' import {logger} from '#/logger'
import {useModalControls} from '#/state/modals' import {useModalControls} from '#/state/modals'
import {useModerationOpts} from '#/state/preferences/moderation-opts'
import {useListMembersQuery} from '#/state/queries/list-members' import {useListMembersQuery} from '#/state/queries/list-members'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {ProfileCard} from '#/view/com/profile/ProfileCard'
import {ErrorMessage} from '#/view/com/util/error/ErrorMessage' 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 {List, type ListRef} from '#/view/com/util/List'
import {ProfileCardFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder' import {ProfileCardFeedLoadingPlaceholder} from '#/view/com/util/LoadingPlaceholder'
import {LoadMoreRetryBtn} from '#/view/com/util/LoadMoreRetryBtn' 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 {ListFooter} from '#/components/Lists'
import * as ProfileCard from '#/components/ProfileCard'
import type * as bsky from '#/types/bsky' import type * as bsky from '#/types/bsky'
const LOADING_ITEM = {_reactKey: '__loading__'} const LOADING_ITEM = {_reactKey: '__loading__'}
@@ -47,11 +48,12 @@ export function ListMembers({
headerOffset?: number headerOffset?: number
desktopFixedHeightOffset?: number desktopFixedHeightOffset?: number
}) { }) {
const t = useTheme()
const {_} = useLingui() const {_} = useLingui()
const [isRefreshing, setIsRefreshing] = React.useState(false) const [isRefreshing, setIsRefreshing] = React.useState(false)
const {isMobile} = useWebMediaQueries()
const {openModal} = useModalControls() const {openModal} = useModalControls()
const {currentAccount} = useSession() const {currentAccount} = useSession()
const moderationOpts = useModerationOpts()
const { const {
data, data,
@@ -131,23 +133,6 @@ export function ListMembers({
// rendering // 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( const renderItem = React.useCallback(
({item}: {item: any}) => { ({item}: {item: any}) => {
if (item === EMPTY_ITEM) { if (item === EMPTY_ITEM) {
@@ -171,26 +156,60 @@ export function ListMembers({
} else if (item === LOADING_ITEM) { } else if (item === LOADING_ITEM) {
return <ProfileCardFeedLoadingPlaceholder /> return <ProfileCardFeedLoadingPlaceholder />
} }
const profile = (item as AppBskyGraphDefs.ListItemView).subject
if (!moderationOpts) return null
return ( return (
<ProfileCard <View
testID={`user-${ style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}>
(item as AppBskyGraphDefs.ListItemView).subject.handle <ProfileCard.Link profile={profile}>
}`} <ProfileCard.Outer>
profile={(item as AppBskyGraphDefs.ListItemView).subject} <ProfileCard.Header>
renderButton={renderMemberButton} <ProfileCard.Avatar
style={{paddingHorizontal: isMobile ? 8 : 14, paddingVertical: 4}} profile={profile}
noModFilter 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, renderEmptyState,
error, error,
onPressTryAgain, onPressTryAgain,
onPressRetryLoadMore, onPressRetryLoadMore,
isMobile, moderationOpts,
isOwner,
onPressEditMembership,
_, _,
t,
], ],
) )
+20 -291
View File
@@ -1,307 +1,36 @@
import React from 'react' import {View} from 'react-native'
import {StyleProp, StyleSheet, View, ViewStyle} from 'react-native' import {type AppBskyActorDefs} from '@atproto/api'
import {
AppBskyActorDefs,
moderateProfile,
ModerationDecision,
} from '@atproto/api'
import {useQueryClient} from '@tanstack/react-query'
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 {useModerationOpts} from '#/state/preferences/moderation-opts'
import {precacheProfile} from '#/state/queries/profile' import {atoms as a, useTheme} from '#/alf'
import {useSession} from '#/state/session' import * as ProfileCard from '#/components/ProfileCard'
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>
)
}
export function ProfileCardWithFollowBtn({ export function ProfileCardWithFollowBtn({
profile, profile,
noBg,
noBorder, noBorder,
onPress,
onFollow,
logContext = 'ProfileCard', logContext = 'ProfileCard',
showKnownFollowers,
}: { }: {
profile: AppBskyActorDefs.ProfileView profile: AppBskyActorDefs.ProfileView
noBg?: boolean
noBorder?: boolean noBorder?: boolean
onPress?: () => void
onFollow?: () => void
logContext?: 'ProfileCard' | 'StarterPackProfilesList' logContext?: 'ProfileCard' | 'StarterPackProfilesList'
showKnownFollowers?: boolean
}) { }) {
const {currentAccount} = useSession() const t = useTheme()
const isMe = profile.did === currentAccount?.did const moderationOpts = useModerationOpts()
if (!moderationOpts) return null
return ( return (
<ProfileCard <View
profile={profile} style={[
noBg={noBg} a.py_md,
noBorder={noBorder} a.px_xl,
renderButton={ !noBorder && [a.border_t, t.atoms.border_contrast_low],
isMe ]}>
? undefined <ProfileCard.Default
: profileShadow => ( profile={profile}
<FollowButton moderationOpts={moderationOpts}
profile={profileShadow} logContext={logContext}
logContext={logContext} />
onFollow={onFollow} </View>
/>
)
}
onPress={onPress}
showKnownFollowers={!isMe && showKnownFollowers}
/>
) )
} }
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 React from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import { import {
AppBskyActorDefs, type AppBskyActorDefs,
AppBskyFeedDefs, type AppBskyFeedDefs,
AppBskyFeedPost, type AppBskyFeedPost,
ComAtprotoLabelDefs, type ComAtprotoLabelDefs,
interpretLabelValueDefinition, interpretLabelValueDefinition,
LabelPreference, type LabelPreference,
LABELS, LABELS,
mock, mock,
moderatePost, moderatePost,
moderateProfile, moderateProfile,
ModerationBehavior, type ModerationBehavior,
ModerationDecision, type ModerationDecision,
ModerationOpts, type ModerationOpts,
RichText, RichText,
} from '@atproto/api' } from '@atproto/api'
import {msg} from '@lingui/macro' import {msg} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useGlobalLabelStrings} from '#/lib/moderation/useGlobalLabelStrings' 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 {moderationOptsOverrideContext} from '#/state/preferences/moderation-opts'
import {FeedNotification} from '#/state/queries/notifications/types' import {type FeedNotification} from '#/state/queries/notifications/types'
import { import {
groupNotifications, groupNotifications,
shouldFilterNotif, shouldFilterNotif,
@@ -42,12 +45,12 @@ import {
ChevronTop_Stroke2_Corner0_Rounded as ChevronTop, ChevronTop_Stroke2_Corner0_Rounded as ChevronTop,
} from '#/components/icons/Chevron' } from '#/components/icons/Chevron'
import * as Layout from '#/components/Layout' import * as Layout from '#/components/Layout'
import * as ProfileCard from '#/components/ProfileCard'
import {H1, H3, P, Text} from '#/components/Typography' import {H1, H3, P, Text} from '#/components/Typography'
import {ScreenHider} from '../../components/moderation/ScreenHider' import {ScreenHider} from '../../components/moderation/ScreenHider'
import {NotificationFeedItem} from '../com/notifications/NotificationFeedItem' import {NotificationFeedItem} from '../com/notifications/NotificationFeedItem'
import {PostThreadItem} from '../com/post-thread/PostThreadItem' import {PostThreadItem} from '../com/post-thread/PostThreadItem'
import {PostFeedItem} from '../com/posts/PostFeedItem' import {PostFeedItem} from '../com/posts/PostFeedItem'
import {ProfileCard} from '../com/profile/ProfileCard'
const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys( const LABEL_VALUES: (keyof typeof LABELS)[] = Object.keys(
LABELS, LABELS,
@@ -890,6 +893,9 @@ function MockAccountCard({
moderation: ModerationDecision moderation: ModerationDecision
}) { }) {
const t = useTheme() const t = useTheme()
const moderationOpts = useModerationOpts()
if (!moderationOpts) return null
if (moderation.ui('profileList').filter) { if (moderation.ui('profileList').filter) {
return ( return (
@@ -899,7 +905,7 @@ function MockAccountCard({
) )
} }
return <ProfileCard profile={profile} /> return <ProfileCard.Card profile={profile} moderationOpts={moderationOpts} />
} }
function MockAccountScreen({ function MockAccountScreen({
+22 -13
View File
@@ -6,35 +6,38 @@ import {
StyleSheet, StyleSheet,
View, View,
} from 'react-native' } 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 {msg, 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 {NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
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 {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'
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 {ProfileCard} from '#/view/com/profile/ProfileCard'
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 {Text} from '#/view/com/util/text/Text'
import {ViewHeader} from '#/view/com/util/ViewHeader' 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 Layout from '#/components/Layout'
import * as ProfileCard from '#/components/ProfileCard'
type Props = NativeStackScreenProps< type Props = NativeStackScreenProps<
CommonNavigatorParams, CommonNavigatorParams,
'ModerationBlockedAccounts' 'ModerationBlockedAccounts'
> >
export function ModerationBlockedAccounts({}: Props) { export function ModerationBlockedAccounts({}: Props) {
const t = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const moderationOpts = useModerationOpts()
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const { const {
@@ -87,14 +90,20 @@ export function ModerationBlockedAccounts({}: Props) {
}: { }: {
item: ActorDefs.ProfileView item: ActorDefs.ProfileView
index: number index: number
}) => ( }) => {
<ProfileCard if (!moderationOpts) return null
testID={`blockedAccount-${index}`} return (
key={item.did} <View
profile={item} style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}
noModFilter key={item.did}>
/> <ProfileCard.Default
) testID={`blockedAccount-${index}`}
profile={item}
moderationOpts={moderationOpts}
/>
</View>
)
}
return ( return (
<Layout.Screen testID="blockedAccountsScreen"> <Layout.Screen testID="blockedAccountsScreen">
<Layout.Center style={[a.flex_1, {paddingBottom: 100}]}> <Layout.Center style={[a.flex_1, {paddingBottom: 100}]}>
+36 -13
View File
@@ -6,35 +6,38 @@ import {
StyleSheet, StyleSheet,
View, View,
} from 'react-native' } 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 {msg, 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 {NativeStackScreenProps} from '@react-navigation/native-stack' import {type NativeStackScreenProps} from '@react-navigation/native-stack'
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 {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'
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 {ProfileCard} from '#/view/com/profile/ProfileCard'
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 {Text} from '#/view/com/util/text/Text'
import {ViewHeader} from '#/view/com/util/ViewHeader' 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 Layout from '#/components/Layout'
import * as ProfileCard from '#/components/ProfileCard'
type Props = NativeStackScreenProps< type Props = NativeStackScreenProps<
CommonNavigatorParams, CommonNavigatorParams,
'ModerationMutedAccounts' 'ModerationMutedAccounts'
> >
export function ModerationMutedAccounts({}: Props) { export function ModerationMutedAccounts({}: Props) {
const t = useTheme()
const pal = usePalette('default') const pal = usePalette('default')
const {_} = useLingui() const {_} = useLingui()
const setMinimalShellMode = useSetMinimalShellMode() const setMinimalShellMode = useSetMinimalShellMode()
const {isTabletOrDesktop} = useWebMediaQueries() const {isTabletOrDesktop} = useWebMediaQueries()
const moderationOpts = useModerationOpts()
const [isPTRing, setIsPTRing] = React.useState(false) const [isPTRing, setIsPTRing] = React.useState(false)
const { const {
@@ -87,14 +90,34 @@ export function ModerationMutedAccounts({}: Props) {
}: { }: {
item: ActorDefs.ProfileView item: ActorDefs.ProfileView
index: number index: number
}) => ( }) => {
<ProfileCard if (!moderationOpts) return null
testID={`mutedAccount-${index}`} return (
key={item.did} <View
profile={item} style={[a.py_md, a.px_xl, a.border_t, t.atoms.border_contrast_low]}
noModFilter 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 ( return (
<Layout.Screen testID="mutedAccountsScreen"> <Layout.Screen testID="mutedAccountsScreen">
<ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop /> <ViewHeader title={_(msg`Muted Accounts`)} showOnDesktop />