Add user/profile labeling
This commit is contained in:
@@ -104,7 +104,7 @@ export const LABEL_VAL_GROUPS: Record<string, LabelValGroup> = {
|
||||
},
|
||||
impersonation: {
|
||||
id: 'impersonation',
|
||||
title: 'Impersonated Content',
|
||||
title: 'Impersonation',
|
||||
values: ['impersonation'],
|
||||
},
|
||||
unknown: {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import {ComAtprotoLabelDefs} from '@atproto/api'
|
||||
import {LabelValGroup, LABEL_VAL_GROUPS} from './const'
|
||||
|
||||
export function getLabelValueGroup(labelVal: string): LabelValGroup {
|
||||
|
||||
@@ -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={
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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]}>
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
|
||||
@@ -155,6 +155,7 @@ export const SearchScreen = withAuthRequired(
|
||||
testID={`searchAutoCompleteResult-${item.handle}`}
|
||||
handle={item.handle}
|
||||
displayName={item.displayName}
|
||||
labels={item.labels}
|
||||
avatar={item.avatar}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user