Add user/profile labeling

This commit is contained in:
Paul Frazee
2023-04-12 15:48:00 -07:00
parent 8a7091e5e7
commit 68fbeef926
21 changed files with 170 additions and 22 deletions
+1 -1
View File
@@ -104,7 +104,7 @@ export const LABEL_VAL_GROUPS: Record<string, LabelValGroup> = {
}, },
impersonation: { impersonation: {
id: 'impersonation', id: 'impersonation',
title: 'Impersonated Content', title: 'Impersonation',
values: ['impersonation'], values: ['impersonation'],
}, },
unknown: { unknown: {
-1
View File
@@ -1,4 +1,3 @@
import {ComAtprotoLabelDefs} from '@atproto/api'
import {LabelValGroup, LABEL_VAL_GROUPS} from './const' import {LabelValGroup, LABEL_VAL_GROUPS} from './const'
export function getLabelValueGroup(labelVal: string): LabelValGroup { export function getLabelValueGroup(labelVal: string): LabelValGroup {
+3
View File
@@ -1,6 +1,7 @@
import {makeAutoObservable, runInAction} from 'mobx' import {makeAutoObservable, runInAction} from 'mobx'
import {PickedMedia} from 'lib/media/picker' import {PickedMedia} from 'lib/media/picker'
import { import {
ComAtprotoLabelDefs,
AppBskyActorGetProfile as GetProfile, AppBskyActorGetProfile as GetProfile,
AppBskyActorProfile, AppBskyActorProfile,
RichText, RichText,
@@ -41,6 +42,7 @@ export class ProfileModel {
followersCount: number = 0 followersCount: number = 0
followsCount: number = 0 followsCount: number = 0
postsCount: number = 0 postsCount: number = 0
labels?: ComAtprotoLabelDefs.Label[] = undefined
viewer = new ProfileViewerModel() viewer = new ProfileViewerModel()
// added data // added data
@@ -210,6 +212,7 @@ export class ProfileModel {
this.followersCount = res.data.followersCount || 0 this.followersCount = res.data.followersCount || 0
this.followsCount = res.data.followsCount || 0 this.followsCount = res.data.followsCount || 0
this.postsCount = res.data.postsCount || 0 this.postsCount = res.data.postsCount || 0
this.labels = res.data.labels
if (res.data.viewer) { if (res.data.viewer) {
Object.assign(this.viewer, res.data.viewer) Object.assign(this.viewer, res.data.viewer)
this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following) this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
@@ -31,6 +31,7 @@ export const SuggestedFollows = ({
handle={item.handle} handle={item.handle}
displayName={item.displayName} displayName={item.displayName}
avatar={item.avatar} avatar={item.avatar}
labels={item.labels}
noBg noBg
noBorder noBorder
description={ description={
+19 -4
View File
@@ -8,7 +8,7 @@ import {
View, View,
} from 'react-native' } from 'react-native'
import {AppBskyEmbedImages} from '@atproto/api' import {AppBskyEmbedImages} from '@atproto/api'
import {AtUri} from '@atproto/api' import {AtUri, ComAtprotoLabelDefs} from '@atproto/api'
import { import {
FontAwesomeIcon, FontAwesomeIcon,
FontAwesomeIconStyle, FontAwesomeIconStyle,
@@ -38,6 +38,7 @@ interface Author {
handle: string handle: string
displayName?: string displayName?: string
avatar?: string avatar?: string
labels?: ComAtprotoLabelDefs.Label[]
} }
export const FeedItem = observer(function FeedItem({ export const FeedItem = observer(function FeedItem({
@@ -129,6 +130,7 @@ export const FeedItem = observer(function FeedItem({
handle: item.author.handle, handle: item.author.handle,
displayName: item.author.displayName, displayName: item.author.displayName,
avatar: item.author.avatar, avatar: item.author.avatar,
labels: item.author.labels,
}, },
] ]
if (item.additional?.length) { if (item.additional?.length) {
@@ -138,6 +140,7 @@ export const FeedItem = observer(function FeedItem({
handle: item2.author.handle, handle: item2.author.handle,
displayName: item2.author.displayName, displayName: item2.author.displayName,
avatar: item2.author.avatar, avatar: item2.author.avatar,
labels: item.author.labels,
})), })),
) )
} }
@@ -255,7 +258,11 @@ function CondensedAuthorsList({
href={authors[0].href} href={authors[0].href}
title={`@${authors[0].handle}`} title={`@${authors[0].handle}`}
asAnchor> asAnchor>
<UserAvatar size={35} avatar={authors[0].avatar} /> <UserAvatar
size={35}
avatar={authors[0].avatar}
hasWarning={!!authors[0].labels?.length}
/>
</Link> </Link>
</View> </View>
) )
@@ -264,7 +271,11 @@ function CondensedAuthorsList({
<View style={styles.avis}> <View style={styles.avis}>
{authors.slice(0, MAX_AUTHORS).map(author => ( {authors.slice(0, MAX_AUTHORS).map(author => (
<View key={author.href} style={s.mr5}> <View key={author.href} style={s.mr5}>
<UserAvatar size={35} avatar={author.avatar} /> <UserAvatar
size={35}
avatar={author.avatar}
hasWarning={!!author.labels?.length}
/>
</View> </View>
))} ))}
{authors.length > MAX_AUTHORS ? ( {authors.length > MAX_AUTHORS ? (
@@ -317,7 +328,11 @@ function ExpandedAuthorsList({
style={styles.expandedAuthor} style={styles.expandedAuthor}
asAnchor> asAnchor>
<View style={styles.expandedAuthorAvi}> <View style={styles.expandedAuthorAvi}>
<UserAvatar size={35} avatar={author.avatar} /> <UserAvatar
size={35}
avatar={author.avatar}
hasWarning={!!author.labels?.length}
/>
</View> </View>
<View style={s.flex1}> <View style={s.flex1}>
<Text <Text
+1
View File
@@ -53,6 +53,7 @@ export const PostLikedBy = observer(function ({uri}: {uri: string}) {
handle={item.actor.handle} handle={item.actor.handle}
displayName={item.actor.displayName} displayName={item.actor.displayName}
avatar={item.actor.avatar} avatar={item.actor.avatar}
labels={item.actor.labels}
isFollowedBy={!!item.actor.viewer?.followedBy} isFollowedBy={!!item.actor.viewer?.followedBy}
/> />
) )
@@ -64,6 +64,7 @@ export const PostRepostedBy = observer(function PostRepostedBy({
handle={item.handle} handle={item.handle}
displayName={item.displayName} displayName={item.displayName}
avatar={item.avatar} avatar={item.avatar}
labels={item.labels}
isFollowedBy={!!item.viewer?.followedBy} isFollowedBy={!!item.viewer?.followedBy}
/> />
) )
+11 -2
View File
@@ -138,7 +138,11 @@ export const PostThreadItem = observer(function PostThreadItem({
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle} asAnchor> <Link href={authorHref} title={authorTitle} asAnchor>
<UserAvatar size={52} avatar={item.post.author.avatar} /> <UserAvatar
size={52}
avatar={item.post.author.avatar}
hasWarning={!!item.post.author.labels?.length}
/>
</Link> </Link>
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
@@ -304,13 +308,18 @@ export const PostThreadItem = observer(function PostThreadItem({
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle} asAnchor> <Link href={authorHref} title={authorTitle} asAnchor>
<UserAvatar size={52} avatar={item.post.author.avatar} /> <UserAvatar
size={52}
avatar={item.post.author.avatar}
hasWarning={!!item.post.author.labels?.length}
/>
</Link> </Link>
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<PostMeta <PostMeta
authorHandle={item.post.author.handle} authorHandle={item.post.author.handle}
authorDisplayName={item.post.author.displayName} authorDisplayName={item.post.author.displayName}
authorHasWarning={!!item.post.author.labels?.length}
timestamp={item.post.indexedAt} timestamp={item.post.indexedAt}
postHref={itemHref} postHref={itemHref}
did={item.post.author.did} did={item.post.author.did}
+6 -2
View File
@@ -96,7 +96,6 @@ export const Post = observer(function Post({
return ( return (
<PostLoaded <PostLoaded
view={view}
item={view.thread} item={view.thread}
record={view.thread.postRecord} record={view.thread.postRecord}
setDeleted={setDeleted} setDeleted={setDeleted}
@@ -198,13 +197,18 @@ const PostLoaded = observer(
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Link href={authorHref} title={authorTitle} asAnchor> <Link href={authorHref} title={authorTitle} asAnchor>
<UserAvatar size={52} avatar={item.post.author.avatar} /> <UserAvatar
size={52}
avatar={item.post.author.avatar}
hasWarning={!!item.post.author.labels?.length}
/>
</Link> </Link>
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<PostMeta <PostMeta
authorHandle={item.post.author.handle} authorHandle={item.post.author.handle}
authorDisplayName={item.post.author.displayName} authorDisplayName={item.post.author.displayName}
authorHasWarning={!!item.post.author.labels?.length}
timestamp={item.post.indexedAt} timestamp={item.post.indexedAt}
postHref={itemHref} postHref={itemHref}
did={item.post.author.did} did={item.post.author.did}
+6 -1
View File
@@ -189,13 +189,18 @@ export const FeedItem = observer(function ({
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<Link href={authorHref} title={item.post.author.handle} asAnchor> <Link href={authorHref} title={item.post.author.handle} asAnchor>
<UserAvatar size={52} avatar={item.post.author.avatar} /> <UserAvatar
size={52}
avatar={item.post.author.avatar}
hasWarning={!!item.post.author.labels?.length}
/>
</Link> </Link>
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<PostMeta <PostMeta
authorHandle={item.post.author.handle} authorHandle={item.post.author.handle}
authorDisplayName={item.post.author.displayName} authorDisplayName={item.post.author.displayName}
authorHasWarning={!!item.post.author.labels?.length}
timestamp={item.post.indexedAt} timestamp={item.post.indexedAt}
postHref={itemHref} postHref={itemHref}
did={item.post.author.did} did={item.post.author.did}
+7 -2
View File
@@ -1,7 +1,7 @@
import React from 'react' import React from 'react'
import {StyleSheet, View} from 'react-native' import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite' import {observer} from 'mobx-react-lite'
import {AppBskyActorDefs} from '@atproto/api' import {AppBskyActorDefs, ComAtprotoLabelDefs} from '@atproto/api'
import {Link} from '../util/Link' import {Link} from '../util/Link'
import {Text} from '../util/text/Text' import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
@@ -17,6 +17,7 @@ export function ProfileCard({
displayName, displayName,
avatar, avatar,
description, description,
labels,
isFollowedBy, isFollowedBy,
noBg, noBg,
noBorder, noBorder,
@@ -28,6 +29,7 @@ export function ProfileCard({
displayName?: string displayName?: string
avatar?: string avatar?: string
description?: string description?: string
labels: ComAtprotoLabelDefs.Label[] | undefined
isFollowedBy?: boolean isFollowedBy?: boolean
noBg?: boolean noBg?: boolean
noBorder?: boolean noBorder?: boolean
@@ -50,7 +52,7 @@ export function ProfileCard({
asAnchor> asAnchor>
<View style={styles.layout}> <View style={styles.layout}>
<View style={styles.layoutAvi}> <View style={styles.layoutAvi}>
<UserAvatar size={40} avatar={avatar} /> <UserAvatar size={40} avatar={avatar} hasWarning={!!labels?.length} />
</View> </View>
<View style={styles.layoutContent}> <View style={styles.layoutContent}>
<Text <Text
@@ -114,6 +116,7 @@ export const ProfileCardWithFollowBtn = observer(
displayName, displayName,
avatar, avatar,
description, description,
labels,
isFollowedBy, isFollowedBy,
noBg, noBg,
noBorder, noBorder,
@@ -124,6 +127,7 @@ export const ProfileCardWithFollowBtn = observer(
displayName?: string displayName?: string
avatar?: string avatar?: string
description?: string description?: string
labels: ComAtprotoLabelDefs.Label[] | undefined
isFollowedBy?: boolean isFollowedBy?: boolean
noBg?: boolean noBg?: boolean
noBorder?: boolean noBorder?: boolean
@@ -138,6 +142,7 @@ export const ProfileCardWithFollowBtn = observer(
displayName={displayName} displayName={displayName}
avatar={avatar} avatar={avatar}
description={description} description={description}
labels={labels}
isFollowedBy={isFollowedBy} isFollowedBy={isFollowedBy}
noBg={noBg} noBg={noBg}
noBorder={noBorder} noBorder={noBorder}
@@ -67,6 +67,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
handle={item.handle} handle={item.handle}
displayName={item.displayName} displayName={item.displayName}
avatar={item.avatar} avatar={item.avatar}
labels={item.labels}
isFollowedBy={!!item.viewer?.followedBy} isFollowedBy={!!item.viewer?.followedBy}
/> />
) )
+1
View File
@@ -64,6 +64,7 @@ export const ProfileFollows = observer(function ProfileFollows({
handle={item.handle} handle={item.handle}
displayName={item.displayName} displayName={item.displayName}
avatar={item.avatar} avatar={item.avatar}
labels={item.labels}
isFollowedBy={!!item.viewer?.followedBy} isFollowedBy={!!item.viewer?.followedBy}
/> />
) )
+7 -1
View File
@@ -27,6 +27,7 @@ import {Text} from '../util/text/Text'
import {RichText} from '../util/text/RichText' import {RichText} from '../util/text/RichText'
import {UserAvatar} from '../util/UserAvatar' import {UserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner' import {UserBanner} from '../util/UserBanner'
import {ProfileHeaderLabels} from '../util/moderation/ProfileHeaderLabels'
import {usePalette} from 'lib/hooks/usePalette' import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics' import {useAnalytics} from 'lib/analytics'
import {NavigationProp} from 'lib/routes/types' import {NavigationProp} from 'lib/routes/types'
@@ -320,6 +321,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
richText={view.descriptionRichText} richText={view.descriptionRichText}
/> />
) : undefined} ) : undefined}
<ProfileHeaderLabels labels={view.labels} />
{view.viewer.muted ? ( {view.viewer.muted ? (
<View <View
testID="profileHeaderMutedNotice" testID="profileHeaderMutedNotice"
@@ -348,7 +350,11 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
onPress={onPressAvi}> onPress={onPressAvi}>
<View <View
style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}> style={[pal.view, {borderColor: pal.colors.background}, styles.avi]}>
<UserAvatar size={80} avatar={view.avatar} /> <UserAvatar
size={80}
avatar={view.avatar}
hasWarning={!!view.labels?.length}
/>
</View> </View>
</TouchableWithoutFeedback> </TouchableWithoutFeedback>
</View> </View>
+1
View File
@@ -101,6 +101,7 @@ const Profiles = observer(({model}: {model: SearchUIModel}) => {
displayName={item.displayName} displayName={item.displayName}
avatar={item.avatar} avatar={item.avatar}
description={item.description} description={item.description}
labels={item.labels}
/> />
))} ))}
<View style={s.footerSpacer} /> <View style={s.footerSpacer} />
+6 -1
View File
@@ -15,6 +15,7 @@ interface PostMetaOpts {
authorAvatar?: string authorAvatar?: string
authorHandle: string authorHandle: string
authorDisplayName: string | undefined authorDisplayName: string | undefined
authorHasWarning: boolean
postHref: string postHref: string
timestamp: string timestamp: string
did?: string did?: string
@@ -93,7 +94,11 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
<View style={styles.meta}> <View style={styles.meta}>
{typeof opts.authorAvatar !== 'undefined' && ( {typeof opts.authorAvatar !== 'undefined' && (
<View style={[styles.metaItem, styles.avatar]}> <View style={[styles.metaItem, styles.avatar]}>
<UserAvatar avatar={opts.authorAvatar} size={16} /> <UserAvatar
avatar={opts.authorAvatar}
size={16}
hasWarning={opts.authorHasWarning}
/>
</View> </View>
)} )}
<View style={[styles.metaItem, styles.maxWidth]}> <View style={[styles.metaItem, styles.maxWidth]}>
+40 -7
View File
@@ -44,10 +44,12 @@ function DefaultAvatar({size}: {size: number}) {
export function UserAvatar({ export function UserAvatar({
size, size,
avatar, avatar,
hasWarning,
onSelectNewAvatar, onSelectNewAvatar,
}: { }: {
size: number size: number
avatar?: string | null avatar?: string | null
hasWarning?: boolean
onSelectNewAvatar?: (img: PickedMedia | null) => void onSelectNewAvatar?: (img: PickedMedia | null) => void
}) { }) {
const store = useStores() const store = useStores()
@@ -105,6 +107,22 @@ export function UserAvatar({
}, },
}, },
] ]
const warning = React.useMemo(() => {
if (!hasWarning) {
return <></>
}
return (
<View style={[styles.warningIconContainer, pal.view]}>
<FontAwesomeIcon
icon="exclamation-circle"
style={styles.warningIcon}
size={Math.floor(size / 3)}
/>
</View>
)
}, [hasWarning, size, pal])
// onSelectNewAvatar is only passed as prop on the EditProfile component // onSelectNewAvatar is only passed as prop on the EditProfile component
return onSelectNewAvatar ? ( return onSelectNewAvatar ? (
<DropdownButton <DropdownButton
@@ -137,14 +155,20 @@ export function UserAvatar({
</View> </View>
</DropdownButton> </DropdownButton>
) : avatar ? ( ) : avatar ? (
<HighPriorityImage <View style={{width: size, height: size}}>
testID="userAvatarImage" <HighPriorityImage
style={{width: size, height: size, borderRadius: Math.floor(size / 2)}} testID="userAvatarImage"
resizeMode="stretch" style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
source={{uri: avatar}} resizeMode="stretch"
/> source={{uri: avatar}}
/>
{warning}
</View>
) : ( ) : (
<DefaultAvatar size={size} /> <View style={{width: size, height: size}}>
<DefaultAvatar size={size} />
{warning}
</View>
) )
} }
@@ -165,4 +189,13 @@ const styles = StyleSheet.create({
height: 80, height: 80,
borderRadius: 40, borderRadius: 40,
}, },
warningIconContainer: {
position: 'absolute',
right: 0,
bottom: 0,
borderRadius: 100,
},
warningIcon: {
color: colors.red3,
},
}) })
@@ -0,0 +1,55 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {ComAtprotoLabelDefs} from '@atproto/api'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
} from '@fortawesome/react-native-fontawesome'
import {Text} from '../text/Text'
import {usePalette} from 'lib/hooks/usePalette'
import {getLabelValueGroup} from 'lib/labeling/helpers'
export function ProfileHeaderLabels({
labels,
}: {
labels: ComAtprotoLabelDefs.Label[] | undefined
}) {
const palErr = usePalette('error')
if (!labels?.length) {
return null
}
return (
<>
{labels.map((label, i) => {
const labelGroup = getLabelValueGroup(label?.val || '')
return (
<View
key={`${label.val}-${i}`}
style={[styles.container, palErr.border, palErr.view]}>
<FontAwesomeIcon
icon="circle-exclamation"
style={palErr.text as FontAwesomeIconStyle}
size={20}
/>
<Text style={palErr.text}>
This account has been flagged for{' '}
{labelGroup.title.toLocaleLowerCase()}.
</Text>
</View>
)
})}
</>
)
}
const styles = StyleSheet.create({
container: {
flexDirection: 'row',
alignItems: 'center',
gap: 10,
borderWidth: 1,
borderRadius: 6,
paddingHorizontal: 10,
paddingVertical: 8,
},
})
@@ -42,6 +42,7 @@ export function QuoteEmbed({
authorAvatar={quote.author.avatar} authorAvatar={quote.author.avatar}
authorHandle={quote.author.handle} authorHandle={quote.author.handle}
authorDisplayName={quote.author.displayName} authorDisplayName={quote.author.displayName}
authorHasWarning={false}
postHref={itemHref} postHref={itemHref}
timestamp={quote.indexedAt} timestamp={quote.indexedAt}
/> />
+1
View File
@@ -155,6 +155,7 @@ export const SearchScreen = withAuthRequired(
testID={`searchAutoCompleteResult-${item.handle}`} testID={`searchAutoCompleteResult-${item.handle}`}
handle={item.handle} handle={item.handle}
displayName={item.displayName} displayName={item.displayName}
labels={item.labels}
avatar={item.avatar} avatar={item.avatar}
/> />
))} ))}
+1
View File
@@ -90,6 +90,7 @@ export const DesktopSearch = observer(function DesktopSearch() {
handle={item.handle} handle={item.handle}
displayName={item.displayName} displayName={item.displayName}
avatar={item.avatar} avatar={item.avatar}
labels={item.labels}
noBorder={i === 0} noBorder={i === 0}
/> />
))} ))}