Refactor moderation to apply to accounts, profiles, and posts correctly (#548)

* Add ScreenHider component

* Add blur attribute to UserAvatar and UserBanner

* Remove dead suggested posts component and model

* Bump @atproto/api@0.2.10

* Rework moderation tooling to give a more precise DSL

* Add label mocks

* Apply finer grained moderation controls

* Refactor ProfileCard to just take the profile object

* Apply moderation to user listings and banner

* Apply moderation to notifications

* Fix lint

* Tune avatar & banner blur settings per platform

* 1.24
This commit is contained in:
Paul Frazee 2023-04-27 12:38:23 -05:00 committed by GitHub
parent 51be8474db
commit 1d50ddb378
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
40 changed files with 1195 additions and 763 deletions

View file

@ -1,7 +1,7 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {AppBskyActorDefs, ComAtprotoLabelDefs} from '@atproto/api'
import {AppBskyActorDefs} from '@atproto/api'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
@ -10,143 +10,159 @@ import {usePalette} from 'lib/hooks/usePalette'
import {useStores} from 'state/index'
import {FollowButton} from './FollowButton'
import {sanitizeDisplayName} from 'lib/strings/display-names'
import {
getProfileViewBasicLabelInfo,
getProfileModeration,
} from 'lib/labeling/helpers'
import {ModerationBehaviorCode} from 'lib/labeling/types'
export function ProfileCard({
testID,
handle,
displayName,
avatar,
description,
labels,
isFollowedBy,
noBg,
noBorder,
followers,
renderButton,
}: {
testID?: string
handle: string
displayName?: string
avatar?: string
description?: string
labels: ComAtprotoLabelDefs.Label[] | undefined
isFollowedBy?: boolean
noBg?: boolean
noBorder?: boolean
followers?: AppBskyActorDefs.ProfileView[] | undefined
renderButton?: () => JSX.Element
}) {
const pal = usePalette('default')
return (
<Link
testID={testID}
style={[
styles.outer,
pal.border,
noBorder && styles.outerNoBorder,
!noBg && pal.view,
]}
href={`/profile/${handle}`}
title={handle}
asAnchor>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<UserAvatar size={40} avatar={avatar} hasWarning={!!labels?.length} />
</View>
<View style={styles.layoutContent}>
<Text
type="lg"
style={[s.bold, pal.text]}
numberOfLines={1}
lineHeight={1.2}>
{sanitizeDisplayName(displayName || handle)}
</Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
@{handle}
</Text>
{isFollowedBy && (
<View style={s.flexRow}>
<View style={[s.mt5, pal.btn, styles.pill]}>
<Text type="xs" style={pal.text}>
Follows You
</Text>
export const ProfileCard = observer(
({
testID,
profile,
noBg,
noBorder,
followers,
renderButton,
}: {
testID?: string
profile: AppBskyActorDefs.ProfileViewBasic
noBg?: boolean
noBorder?: boolean
followers?: AppBskyActorDefs.ProfileView[] | undefined
renderButton?: () => JSX.Element
}) => {
const store = useStores()
const pal = usePalette('default')
const moderation = getProfileModeration(
store,
getProfileViewBasicLabelInfo(profile),
)
if (moderation.list.behavior === ModerationBehaviorCode.Hide) {
return null
}
return (
<Link
testID={testID}
style={[
styles.outer,
pal.border,
noBorder && styles.outerNoBorder,
!noBg && pal.view,
]}
href={`/profile/${profile.handle}`}
title={profile.handle}
asAnchor>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<UserAvatar
size={40}
avatar={profile.avatar}
moderation={moderation.avatar}
/>
</View>
<View style={styles.layoutContent}>
<Text
type="lg"
style={[s.bold, pal.text]}
numberOfLines={1}
lineHeight={1.2}>
{sanitizeDisplayName(profile.displayName || profile.handle)}
</Text>
<Text type="md" style={[pal.textLight]} numberOfLines={1}>
@{profile.handle}
</Text>
{!!profile.viewer?.followedBy && (
<View style={s.flexRow}>
<View style={[s.mt5, pal.btn, styles.pill]}>
<Text type="xs" style={pal.text}>
Follows You
</Text>
</View>
</View>
</View>
)}
)}
</View>
{renderButton ? (
<View style={styles.layoutButton}>{renderButton()}</View>
) : undefined}
</View>
{renderButton ? (
<View style={styles.layoutButton}>{renderButton()}</View>
{profile.description ? (
<View style={styles.details}>
<Text style={pal.text} numberOfLines={4}>
{profile.description}
</Text>
</View>
) : undefined}
</View>
{description ? (
<View style={styles.details}>
<Text style={pal.text} numberOfLines={4}>
{description}
</Text>
</View>
) : undefined}
{followers?.length ? (
<View style={styles.followedBy}>
<Text
type="sm"
style={[styles.followsByDesc, pal.textLight]}
numberOfLines={2}
lineHeight={1.2}>
Followed by{' '}
{followers.map(f => f.displayName || f.handle).join(', ')}
</Text>
{followers.slice(0, 3).map(f => (
<View key={f.did} style={styles.followedByAviContainer}>
<View style={[styles.followedByAvi, pal.view]}>
<UserAvatar avatar={f.avatar} size={32} />
</View>
<FollowersList followers={followers} />
</Link>
)
},
)
const FollowersList = observer(
({followers}: {followers?: AppBskyActorDefs.ProfileView[] | undefined}) => {
const store = useStores()
const pal = usePalette('default')
if (!followers?.length) {
return null
}
const followersWithMods = followers
.map(f => ({
f,
mod: getProfileModeration(store, getProfileViewBasicLabelInfo(f)),
}))
.filter(({mod}) => mod.list.behavior !== ModerationBehaviorCode.Hide)
return (
<View style={styles.followedBy}>
<Text
type="sm"
style={[styles.followsByDesc, pal.textLight]}
numberOfLines={2}
lineHeight={1.2}>
Followed by{' '}
{followersWithMods.map(({f}) => f.displayName || f.handle).join(', ')}
</Text>
{followersWithMods.slice(0, 3).map(({f, mod}) => (
<View key={f.did} style={styles.followedByAviContainer}>
<View style={[styles.followedByAvi, pal.view]}>
<UserAvatar avatar={f.avatar} size={32} moderation={mod.avatar} />
</View>
))}
</View>
) : undefined}
</Link>
)
}
</View>
))}
</View>
)
},
)
export const ProfileCardWithFollowBtn = observer(
({
did,
handle,
displayName,
avatar,
description,
labels,
isFollowedBy,
profile,
noBg,
noBorder,
followers,
}: {
did: string
handle: string
displayName?: string
avatar?: string
description?: string
labels: ComAtprotoLabelDefs.Label[] | undefined
isFollowedBy?: boolean
profile: AppBskyActorDefs.ProfileViewBasic
noBg?: boolean
noBorder?: boolean
followers?: AppBskyActorDefs.ProfileView[] | undefined
}) => {
const store = useStores()
const isMe = store.me.handle === handle
const isMe = store.me.handle === profile.handle
return (
<ProfileCard
handle={handle}
displayName={displayName}
avatar={avatar}
description={description}
labels={labels}
isFollowedBy={isFollowedBy}
profile={profile}
noBg={noBg}
noBorder={noBorder}
followers={followers}
renderButton={isMe ? undefined : () => <FollowButton did={did} />}
renderButton={
isMe ? undefined : () => <FollowButton did={profile.did} />
}
/>
)
},

View file

@ -61,15 +61,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
// loaded
// =
const renderItem = ({item}: {item: FollowerItem}) => (
<ProfileCardWithFollowBtn
key={item.did}
did={item.did}
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
labels={item.labels}
isFollowedBy={!!item.viewer?.followedBy}
/>
<ProfileCardWithFollowBtn key={item.did} profile={item} />
)
return (
<FlatList

View file

@ -58,15 +58,7 @@ export const ProfileFollows = observer(function ProfileFollows({
// loaded
// =
const renderItem = ({item}: {item: FollowItem}) => (
<ProfileCardWithFollowBtn
key={item.did}
did={item.did}
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
labels={item.labels}
isFollowedBy={!!item.viewer?.followedBy}
/>
<ProfileCardWithFollowBtn key={item.did} profile={item} />
)
return (
<FlatList

View file

@ -26,7 +26,7 @@ import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText'
import {UserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner'
import {ProfileHeaderLabels} from '../util/moderation/ProfileHeaderLabels'
import {ProfileHeaderWarnings} from '../util/moderation/ProfileHeaderWarnings'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics'
import {NavigationProp} from 'lib/routes/types'
@ -219,7 +219,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
])
return (
<View style={pal.view}>
<UserBanner banner={view.banner} />
<UserBanner banner={view.banner} moderation={view.moderation.avatar} />
<View style={styles.content}>
<View style={[styles.buttonsLine]}>
{isMe ? (
@ -332,7 +332,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
richText={view.descriptionRichText}
/>
) : undefined}
<ProfileHeaderLabels labels={view.labels} />
<ProfileHeaderWarnings moderation={view.moderation.view} />
{view.viewer.muted ? (
<View
testID="profileHeaderMutedNotice"
@ -364,7 +364,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
<UserAvatar
size={80}
avatar={view.avatar}
hasWarning={!!view.labels?.length}
moderation={view.moderation.avatar}
/>
</View>
</TouchableWithoutFeedback>