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: {
id: 'impersonation',
title: 'Impersonated Content',
title: 'Impersonation',
values: ['impersonation'],
},
unknown: {
-1
View File
@@ -1,4 +1,3 @@
import {ComAtprotoLabelDefs} from '@atproto/api'
import {LabelValGroup, LABEL_VAL_GROUPS} from './const'
export function getLabelValueGroup(labelVal: string): LabelValGroup {
+3
View File
@@ -1,6 +1,7 @@
import {makeAutoObservable, runInAction} from 'mobx'
import {PickedMedia} from 'lib/media/picker'
import {
ComAtprotoLabelDefs,
AppBskyActorGetProfile as GetProfile,
AppBskyActorProfile,
RichText,
@@ -41,6 +42,7 @@ export class ProfileModel {
followersCount: number = 0
followsCount: number = 0
postsCount: number = 0
labels?: ComAtprotoLabelDefs.Label[] = undefined
viewer = new ProfileViewerModel()
// added data
@@ -210,6 +212,7 @@ export class ProfileModel {
this.followersCount = res.data.followersCount || 0
this.followsCount = res.data.followsCount || 0
this.postsCount = res.data.postsCount || 0
this.labels = res.data.labels
if (res.data.viewer) {
Object.assign(this.viewer, res.data.viewer)
this.rootStore.me.follows.hydrate(this.did, res.data.viewer.following)
@@ -31,6 +31,7 @@ export const SuggestedFollows = ({
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
labels={item.labels}
noBg
noBorder
description={
+19 -4
View File
@@ -8,7 +8,7 @@ import {
View,
} from 'react-native'
import {AppBskyEmbedImages} from '@atproto/api'
import {AtUri} from '@atproto/api'
import {AtUri, ComAtprotoLabelDefs} from '@atproto/api'
import {
FontAwesomeIcon,
FontAwesomeIconStyle,
@@ -38,6 +38,7 @@ interface Author {
handle: string
displayName?: string
avatar?: string
labels?: ComAtprotoLabelDefs.Label[]
}
export const FeedItem = observer(function FeedItem({
@@ -129,6 +130,7 @@ export const FeedItem = observer(function FeedItem({
handle: item.author.handle,
displayName: item.author.displayName,
avatar: item.author.avatar,
labels: item.author.labels,
},
]
if (item.additional?.length) {
@@ -138,6 +140,7 @@ export const FeedItem = observer(function FeedItem({
handle: item2.author.handle,
displayName: item2.author.displayName,
avatar: item2.author.avatar,
labels: item.author.labels,
})),
)
}
@@ -255,7 +258,11 @@ function CondensedAuthorsList({
href={authors[0].href}
title={`@${authors[0].handle}`}
asAnchor>
<UserAvatar size={35} avatar={authors[0].avatar} />
<UserAvatar
size={35}
avatar={authors[0].avatar}
hasWarning={!!authors[0].labels?.length}
/>
</Link>
</View>
)
@@ -264,7 +271,11 @@ function CondensedAuthorsList({
<View style={styles.avis}>
{authors.slice(0, MAX_AUTHORS).map(author => (
<View key={author.href} style={s.mr5}>
<UserAvatar size={35} avatar={author.avatar} />
<UserAvatar
size={35}
avatar={author.avatar}
hasWarning={!!author.labels?.length}
/>
</View>
))}
{authors.length > MAX_AUTHORS ? (
@@ -317,7 +328,11 @@ function ExpandedAuthorsList({
style={styles.expandedAuthor}
asAnchor>
<View style={styles.expandedAuthorAvi}>
<UserAvatar size={35} avatar={author.avatar} />
<UserAvatar
size={35}
avatar={author.avatar}
hasWarning={!!author.labels?.length}
/>
</View>
<View style={s.flex1}>
<Text
+1
View File
@@ -53,6 +53,7 @@ export const PostLikedBy = observer(function ({uri}: {uri: string}) {
handle={item.actor.handle}
displayName={item.actor.displayName}
avatar={item.actor.avatar}
labels={item.actor.labels}
isFollowedBy={!!item.actor.viewer?.followedBy}
/>
)
@@ -64,6 +64,7 @@ export const PostRepostedBy = observer(function PostRepostedBy({
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
labels={item.labels}
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.layoutAvi}>
<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>
</View>
<View style={styles.layoutContent}>
@@ -304,13 +308,18 @@ export const PostThreadItem = observer(function PostThreadItem({
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<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>
</View>
<View style={styles.layoutContent}>
<PostMeta
authorHandle={item.post.author.handle}
authorDisplayName={item.post.author.displayName}
authorHasWarning={!!item.post.author.labels?.length}
timestamp={item.post.indexedAt}
postHref={itemHref}
did={item.post.author.did}
+6 -2
View File
@@ -96,7 +96,6 @@ export const Post = observer(function Post({
return (
<PostLoaded
view={view}
item={view.thread}
record={view.thread.postRecord}
setDeleted={setDeleted}
@@ -198,13 +197,18 @@ const PostLoaded = observer(
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<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>
</View>
<View style={styles.layoutContent}>
<PostMeta
authorHandle={item.post.author.handle}
authorDisplayName={item.post.author.displayName}
authorHasWarning={!!item.post.author.labels?.length}
timestamp={item.post.indexedAt}
postHref={itemHref}
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.layoutAvi}>
<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>
</View>
<View style={styles.layoutContent}>
<PostMeta
authorHandle={item.post.author.handle}
authorDisplayName={item.post.author.displayName}
authorHasWarning={!!item.post.author.labels?.length}
timestamp={item.post.indexedAt}
postHref={itemHref}
did={item.post.author.did}
+7 -2
View File
@@ -1,7 +1,7 @@
import React from 'react'
import {StyleSheet, View} from 'react-native'
import {observer} from 'mobx-react-lite'
import {AppBskyActorDefs} from '@atproto/api'
import {AppBskyActorDefs, ComAtprotoLabelDefs} from '@atproto/api'
import {Link} from '../util/Link'
import {Text} from '../util/text/Text'
import {UserAvatar} from '../util/UserAvatar'
@@ -17,6 +17,7 @@ export function ProfileCard({
displayName,
avatar,
description,
labels,
isFollowedBy,
noBg,
noBorder,
@@ -28,6 +29,7 @@ export function ProfileCard({
displayName?: string
avatar?: string
description?: string
labels: ComAtprotoLabelDefs.Label[] | undefined
isFollowedBy?: boolean
noBg?: boolean
noBorder?: boolean
@@ -50,7 +52,7 @@ export function ProfileCard({
asAnchor>
<View style={styles.layout}>
<View style={styles.layoutAvi}>
<UserAvatar size={40} avatar={avatar} />
<UserAvatar size={40} avatar={avatar} hasWarning={!!labels?.length} />
</View>
<View style={styles.layoutContent}>
<Text
@@ -114,6 +116,7 @@ export const ProfileCardWithFollowBtn = observer(
displayName,
avatar,
description,
labels,
isFollowedBy,
noBg,
noBorder,
@@ -124,6 +127,7 @@ export const ProfileCardWithFollowBtn = observer(
displayName?: string
avatar?: string
description?: string
labels: ComAtprotoLabelDefs.Label[] | undefined
isFollowedBy?: boolean
noBg?: boolean
noBorder?: boolean
@@ -138,6 +142,7 @@ export const ProfileCardWithFollowBtn = observer(
displayName={displayName}
avatar={avatar}
description={description}
labels={labels}
isFollowedBy={isFollowedBy}
noBg={noBg}
noBorder={noBorder}
@@ -67,6 +67,7 @@ export const ProfileFollowers = observer(function ProfileFollowers({
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
labels={item.labels}
isFollowedBy={!!item.viewer?.followedBy}
/>
)
+1
View File
@@ -64,6 +64,7 @@ export const ProfileFollows = observer(function ProfileFollows({
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
labels={item.labels}
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 {UserAvatar} from '../util/UserAvatar'
import {UserBanner} from '../util/UserBanner'
import {ProfileHeaderLabels} from '../util/moderation/ProfileHeaderLabels'
import {usePalette} from 'lib/hooks/usePalette'
import {useAnalytics} from 'lib/analytics'
import {NavigationProp} from 'lib/routes/types'
@@ -320,6 +321,7 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
richText={view.descriptionRichText}
/>
) : undefined}
<ProfileHeaderLabels labels={view.labels} />
{view.viewer.muted ? (
<View
testID="profileHeaderMutedNotice"
@@ -348,7 +350,11 @@ const ProfileHeaderLoaded = observer(function ProfileHeaderLoaded({
onPress={onPressAvi}>
<View
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>
</TouchableWithoutFeedback>
</View>
+1
View File
@@ -101,6 +101,7 @@ const Profiles = observer(({model}: {model: SearchUIModel}) => {
displayName={item.displayName}
avatar={item.avatar}
description={item.description}
labels={item.labels}
/>
))}
<View style={s.footerSpacer} />
+6 -1
View File
@@ -15,6 +15,7 @@ interface PostMetaOpts {
authorAvatar?: string
authorHandle: string
authorDisplayName: string | undefined
authorHasWarning: boolean
postHref: string
timestamp: string
did?: string
@@ -93,7 +94,11 @@ export const PostMeta = observer(function (opts: PostMetaOpts) {
<View style={styles.meta}>
{typeof opts.authorAvatar !== 'undefined' && (
<View style={[styles.metaItem, styles.avatar]}>
<UserAvatar avatar={opts.authorAvatar} size={16} />
<UserAvatar
avatar={opts.authorAvatar}
size={16}
hasWarning={opts.authorHasWarning}
/>
</View>
)}
<View style={[styles.metaItem, styles.maxWidth]}>
+40 -7
View File
@@ -44,10 +44,12 @@ function DefaultAvatar({size}: {size: number}) {
export function UserAvatar({
size,
avatar,
hasWarning,
onSelectNewAvatar,
}: {
size: number
avatar?: string | null
hasWarning?: boolean
onSelectNewAvatar?: (img: PickedMedia | null) => void
}) {
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
return onSelectNewAvatar ? (
<DropdownButton
@@ -137,14 +155,20 @@ export function UserAvatar({
</View>
</DropdownButton>
) : avatar ? (
<HighPriorityImage
testID="userAvatarImage"
style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
resizeMode="stretch"
source={{uri: avatar}}
/>
<View style={{width: size, height: size}}>
<HighPriorityImage
testID="userAvatarImage"
style={{width: size, height: size, borderRadius: Math.floor(size / 2)}}
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,
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}
authorHandle={quote.author.handle}
authorDisplayName={quote.author.displayName}
authorHasWarning={false}
postHref={itemHref}
timestamp={quote.indexedAt}
/>
+1
View File
@@ -155,6 +155,7 @@ export const SearchScreen = withAuthRequired(
testID={`searchAutoCompleteResult-${item.handle}`}
handle={item.handle}
displayName={item.displayName}
labels={item.labels}
avatar={item.avatar}
/>
))}
+1
View File
@@ -90,6 +90,7 @@ export const DesktopSearch = observer(function DesktopSearch() {
handle={item.handle}
displayName={item.displayName}
avatar={item.avatar}
labels={item.labels}
noBorder={i === 0}
/>
))}