[Chat] Moderation stuff (#10610)

This commit is contained in:
Samuel Newman
2026-05-26 19:55:22 +03:00
committed by GitHub
parent 3f4cd1c683
commit 308b2e05f1
11 changed files with 90 additions and 15 deletions
+19 -1
View File
@@ -1,4 +1,4 @@
import {useEffect} from 'react' import {useEffect, useMemo} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import Animated, { import Animated, {
Easing, Easing,
@@ -8,6 +8,11 @@ import Animated, {
withDelay, withDelay,
withTiming, withTiming,
} from 'react-native-reanimated' } from 'react-native-reanimated'
import {
moderateProfile,
type ModerationOpts,
type ModerationUI,
} from '@atproto/api'
import {useSession} from '#/state/session' import {useSession} from '#/state/session'
import {UserAvatar} from '#/view/com/util/UserAvatar' import {UserAvatar} from '#/view/com/util/UserAvatar'
@@ -27,18 +32,27 @@ type Props = {
animate?: boolean animate?: boolean
profiles: bsky.profile.AnyProfileView[] profiles: bsky.profile.AnyProfileView[]
size?: number size?: number
moderationOpts?: ModerationOpts
} }
export function AvatarBubbles({ export function AvatarBubbles({
animate = false, animate = false,
profiles: allProfiles, profiles: allProfiles,
size = 120, size = 120,
moderationOpts,
}: Props) { }: Props) {
const {currentAccount} = useSession() const {currentAccount} = useSession()
const profiles = const profiles =
allProfiles.length > 2 allProfiles.length > 2
? allProfiles.filter(p => p.did !== currentAccount?.did) ? allProfiles.filter(p => p.did !== currentAccount?.did)
: allProfiles : allProfiles
const moderations = useMemo(() => {
if (!moderationOpts) return []
return profiles.map(p => {
return moderateProfile(p, moderationOpts)
})
}, [profiles, moderationOpts])
const scale = size / 120 const scale = size / 120
const marginOffset = size < 120 ? -2 : 0 const marginOffset = size < 120 ? -2 : 0
@@ -90,6 +104,7 @@ export function AvatarBubbles({
y={layout.y} y={layout.y}
zIndex={layout.zIndex} zIndex={layout.zIndex}
includeProfileBorder={layout.border} includeProfileBorder={layout.border}
moderation={moderations[i]?.ui('avatar')}
/> />
))} ))}
</View> </View>
@@ -105,6 +120,7 @@ function AvatarBubble({
y, y,
zIndex, zIndex,
includeProfileBorder, includeProfileBorder,
moderation,
}: { }: {
profile?: bsky.profile.AnyProfileView profile?: bsky.profile.AnyProfileView
scale: SharedValue<number> scale: SharedValue<number>
@@ -113,6 +129,7 @@ function AvatarBubble({
y: number y: number
zIndex?: number zIndex?: number
includeProfileBorder?: boolean includeProfileBorder?: boolean
moderation?: ModerationUI
}) { }) {
const t = useTheme() const t = useTheme()
@@ -140,6 +157,7 @@ function AvatarBubble({
type="user" type="user"
hideLiveBadge hideLiveBadge
noBorder noBorder
moderation={moderation}
/> />
) : ( ) : (
<AvatarPlaceholder size={size} /> <AvatarPlaceholder size={size} />
@@ -152,7 +152,11 @@ function RecentChatItem({
a.align_center, a.align_center,
]}> ]}>
{convo.kind === 'group' ? ( {convo.kind === 'group' ? (
<AvatarBubbles profiles={convo.members} size={WIDTH - 8} /> <AvatarBubbles
profiles={convo.members}
size={WIDTH - 8}
moderationOpts={moderationOpts}
/>
) : ( ) : (
<UserAvatar <UserAvatar
avatar={primaryProfile.avatar} avatar={primaryProfile.avatar}
@@ -514,7 +514,11 @@ function ExistingChatCard({
]}> ]}>
<ProfileCard.Header> <ProfileCard.Header>
{convo.kind === 'group' ? ( {convo.kind === 'group' ? (
<AvatarBubbles profiles={convo.members} size={40} /> <AvatarBubbles
profiles={convo.members}
size={40}
moderationOpts={moderationOpts}
/>
) : ( ) : (
<ProfileCard.Avatar <ProfileCard.Avatar
profile={convo.primaryMember} profile={convo.primaryMember}
@@ -22,10 +22,12 @@ export function MessagesListBlockedFooter({
recipient: initialRecipient, recipient: initialRecipient,
convoId, convoId,
moderation, moderation,
isGroup,
}: { }: {
recipient: bsky.profile.AnyProfileView recipient: bsky.profile.AnyProfileView
convoId: string convoId: string
moderation: ModerationDecision moderation: ModerationDecision
isGroup: boolean
}) { }) {
const t = useTheme() const t = useTheme()
const {t: l} = useLingui() const {t: l} = useLingui()
@@ -64,9 +66,7 @@ export function MessagesListBlockedFooter({
a.justify_center, a.justify_center,
a.p_lg, a.p_lg,
t.atoms.bg_contrast_50, t.atoms.bg_contrast_50,
{ {borderRadius: 40},
borderRadius: 40,
},
]}> ]}>
<PersonXIcon fill={t.atoms.text.color} size="lg" style={[a.mb_xs]} /> <PersonXIcon fill={t.atoms.text.color} size="lg" style={[a.mb_xs]} />
<Text <Text
@@ -77,9 +77,11 @@ export function MessagesListBlockedFooter({
a.font_semi_bold, a.font_semi_bold,
t.atoms.text, t.atoms.text,
]}> ]}>
{isBlocking {isGroup
? l`You are blocking this person` ? l`You are blocking the chat owner`
: l`This person is blocking you`} : isBlocking
? l`You are blocking this person`
: l`This person is blocking you`}
</Text> </Text>
<Text <Text
style={[ style={[
+8 -2
View File
@@ -46,7 +46,7 @@ export function MessagesListHeader({convo}: {convo?: ConvoWithDetails | null}) {
convo.kind === 'direct' ? ( convo.kind === 'direct' ? (
<ProfileHeaderReady convo={convo} moderationOpts={moderationOpts} /> <ProfileHeaderReady convo={convo} moderationOpts={moderationOpts} />
) : ( ) : (
<GroupHeaderReady convo={convo} /> <GroupHeaderReady convo={convo} moderationOpts={moderationOpts} />
) )
) : ( ) : (
<> <>
@@ -152,8 +152,10 @@ function ProfileHeaderReady({
function GroupHeaderReady({ function GroupHeaderReady({
convo, convo,
moderationOpts,
}: { }: {
convo: Extract<ConvoWithDetails, {kind: 'group'}> convo: Extract<ConvoWithDetails, {kind: 'group'}>
moderationOpts: ModerationOpts
}) { }) {
const {t: l} = useLingui() const {t: l} = useLingui()
@@ -176,7 +178,11 @@ function GroupHeaderReady({
}, },
} }
}> }>
<AvatarBubbles size={40} profiles={convo.members} /> <AvatarBubbles
size={40}
profiles={convo.members}
moderationOpts={moderationOpts}
/>
<View style={[a.flex_row, a.flex_1, a.align_center]}> <View style={[a.flex_row, a.flex_1, a.align_center]}>
<Text <Text
style={[a.text_md, a.font_semi_bold, a.flex_shrink]} style={[a.text_md, a.font_semi_bold, a.flex_shrink]}
+9 -1
View File
@@ -230,11 +230,19 @@ function InnerReady({
let footer: React.ReactNode = null let footer: React.ReactNode = null
if (isDisabled) { if (isDisabled) {
footer = <ChatDisabled /> footer = <ChatDisabled />
} else if (convo && primaryMember && primaryMemberModeration?.blocked) { } else if (
convo &&
primaryMember &&
primaryMemberModeration &&
(convo.kind === 'group'
? primaryMemberModeration?.blockCause?.type === 'blocking'
: primaryMemberModeration?.blocked)
) {
footer = ( footer = (
<MessagesListBlockedFooter <MessagesListBlockedFooter
recipient={primaryMember} recipient={primaryMember}
convoId={convo.view.id} convoId={convo.view.id}
isGroup={convo.kind === 'group'}
moderation={primaryMemberModeration} moderation={primaryMemberModeration}
/> />
) )
@@ -435,7 +435,10 @@ function SettingsHeader({
<View <View
style={[a.px_xl, a.py_4xl, a.border_b, t.atoms.border_contrast_low]}> style={[a.px_xl, a.py_4xl, a.border_b, t.atoms.border_contrast_low]}>
<View style={[a.align_center, a.justify_center]}> <View style={[a.align_center, a.justify_center]}>
<AvatarBubbles profiles={convo.members} /> <AvatarBubbles
profiles={convo.members}
moderationOpts={moderationOpts}
/>
</View> </View>
<Text <Text
style={[ style={[
@@ -204,6 +204,7 @@ function GroupChatItem({
<AvatarBubbles <AvatarBubbles
profiles={convo.members} profiles={convo.members}
size={isWithinSplitView ? 48 : 52} size={isWithinSplitView ? 48 : 52}
moderationOpts={moderationOpts}
/> />
} }
title={chatName} title={chatName}
@@ -88,7 +88,11 @@ export function MessagesListGroupInfoPanel({
return ( return (
<> <>
<View style={[a.align_center, a.justify_center]}> <View style={[a.align_center, a.justify_center]}>
<AvatarBubbles animate={true} profiles={convo.members} /> <AvatarBubbles
animate={true}
profiles={convo.members}
moderationOpts={moderationOpts}
/>
{convo.details.name ? ( {convo.details.name ? (
<Text <Text
style={[a.text_2xl, a.font_bold, a.mt_lg, a.px_lg, t.atoms.text]}> style={[a.text_2xl, a.font_bold, a.mt_lg, a.px_lg, t.atoms.text]}>
+2
View File
@@ -10,6 +10,7 @@ import {findAllProfilesInQueryData as findAllProfilesInExploreFeedPreviewsQueryD
import {findAllProfilesInQueryData as findAllProfilesInContactMatchesQueryData} from '#/state/queries/find-contacts' import {findAllProfilesInQueryData as findAllProfilesInContactMatchesQueryData} from '#/state/queries/find-contacts'
import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '#/state/queries/known-followers' import {findAllProfilesInQueryData as findAllProfilesInKnownFollowersQueryData} from '#/state/queries/known-followers'
import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '#/state/queries/list-members' import {findAllProfilesInQueryData as findAllProfilesInListMembersQueryData} from '#/state/queries/list-members'
import {findAllProfilesInQueryData as findAllProfilesInGetConvoQueryData} from '#/state/queries/messages/conversation'
import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from '#/state/queries/messages/list-conversations' import {findAllProfilesInQueryData as findAllProfilesInListConvosQueryData} from '#/state/queries/messages/list-conversations'
import {findAllProfilesInQueryData as findAllProfilesInMessagesQueryData} from '#/state/queries/messages/list-convo-members' import {findAllProfilesInQueryData as findAllProfilesInMessagesQueryData} from '#/state/queries/messages/list-convo-members'
import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '#/state/queries/my-blocked-accounts' import {findAllProfilesInQueryData as findAllProfilesInMyBlockedAccountsQueryData} from '#/state/queries/my-blocked-accounts'
@@ -266,4 +267,5 @@ function* findProfilesInCache(
yield* findAllProfilesInNotifsQueryData(queryClient, did) yield* findAllProfilesInNotifsQueryData(queryClient, did)
yield* findAllProfilesInContactMatchesQueryData(queryClient, did) yield* findAllProfilesInContactMatchesQueryData(queryClient, did)
yield* findAllProfilesInMessagesQueryData(queryClient, did) yield* findAllProfilesInMessagesQueryData(queryClient, did)
yield* findAllProfilesInGetConvoQueryData(queryClient, did)
} }
+24 -1
View File
@@ -1,4 +1,8 @@
import {type ChatBskyConvoDefs} from '@atproto/api' import {
type ChatBskyActorDefs,
type ChatBskyConvoDefs,
type ChatBskyConvoGetConvo,
} from '@atproto/api'
import { import {
type QueryClient, type QueryClient,
useMutation, useMutation,
@@ -109,3 +113,22 @@ export function useMarkAsReadMutation() {
}, },
}) })
} }
export function* findAllProfilesInQueryData(
queryClient: QueryClient,
did: string,
): Generator<ChatBskyActorDefs.ProfileViewBasic, void> {
const queryDatas = queryClient.getQueriesData<
ChatBskyConvoGetConvo.OutputSchema['convo']
>({
queryKey: [RQKEY_ROOT],
})
for (const [_queryKey, queryData] of queryDatas) {
if (!queryData) continue
for (const member of queryData.members) {
if (member.did === did) {
yield member
}
}
}
}