[Chat] Handle moderation lock override in group chats (#10835)

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Samuel Newman
2026-06-10 21:57:12 +03:00
committed by GitHub
parent 3b5ede37f7
commit 8db30c7d31
8 changed files with 62 additions and 16 deletions
+1 -1
View File
@@ -93,7 +93,7 @@
"prettier": "prettier --check ." "prettier": "prettier --check ."
}, },
"dependencies": { "dependencies": {
"@atproto/api": "0.20.11", "@atproto/api": "0.20.12",
"@atproto/syntax": "0.6.1", "@atproto/syntax": "0.6.1",
"@bitdrift/react-native": "^0.6.8", "@bitdrift/react-native": "^0.6.8",
"@braintree/sanitize-url": "^6.0.2", "@braintree/sanitize-url": "^6.0.2",
+5 -5
View File
@@ -242,8 +242,8 @@ importers:
.: .:
dependencies: dependencies:
'@atproto/api': '@atproto/api':
specifier: 0.20.11 specifier: 0.20.12
version: 0.20.11 version: 0.20.12
'@atproto/syntax': '@atproto/syntax':
specifier: 0.6.1 specifier: 0.6.1
version: 0.6.1 version: 0.6.1
@@ -877,8 +877,8 @@ packages:
graphql: graphql:
optional: true optional: true
'@atproto/api@0.20.11': '@atproto/api@0.20.12':
resolution: {integrity: sha512-1NoVJpBDAdotxo1iMZdMd75JstpdKWgBYOnxfVD4m+52bRjgU4cFg3EOGNUognZntFgL/bIHyEgyN7SJWVf6Ig==} resolution: {integrity: sha512-pNCrl/BSmkjlrVu0W5A9zkOVRIWdWAdajuHX/EyhDZFxu8HExsiQi6j46H5kv85GrNRXS/QtULE+ocSuMtEJfw==}
engines: {node: '>=22'} engines: {node: '>=22'}
'@atproto/common-web@0.5.0': '@atproto/common-web@0.5.0':
@@ -9493,7 +9493,7 @@ snapshots:
'@0no-co/graphql.web@1.2.0': {} '@0no-co/graphql.web@1.2.0': {}
'@atproto/api@0.20.11': '@atproto/api@0.20.12':
dependencies: dependencies:
'@atproto/common-web': 0.5.0 '@atproto/common-web': 0.5.0
'@atproto/lexicon': 0.7.1 '@atproto/lexicon': 0.7.1
@@ -3,6 +3,7 @@ import {Pressable, View} from 'react-native'
import { import {
ChatBskyActorDefs, ChatBskyActorDefs,
ChatBskyConvoDefs, ChatBskyConvoDefs,
ChatBskyConvoUnlockConvo,
type ModerationOpts, type ModerationOpts,
} from '@atproto/api' } from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
@@ -417,6 +418,12 @@ function SettingsHeader({
if (lock) { if (lock) {
logger.error('Failed to lock group chat', {message: e}) logger.error('Failed to lock group chat', {message: e})
Toast.show(l`Failed to lock group chat`, {type: 'error'}) Toast.show(l`Failed to lock group chat`, {type: 'error'})
} else if (
e instanceof ChatBskyConvoUnlockConvo.ConvoLockedByModerationError
) {
Toast.show(l`This chat is locked by a moderation action`, {
type: 'error',
})
} else { } else {
logger.error('Failed to unlock group chat', {message: e}) logger.error('Failed to unlock group chat', {message: e})
Toast.show(l`Failed to unlock group chat`, {type: 'error'}) Toast.show(l`Failed to unlock group chat`, {type: 'error'})
@@ -447,7 +454,11 @@ function SettingsHeader({
const createdAt = new Date(convo.details.createdAt) const createdAt = new Date(convo.details.createdAt)
const canLockGroupChat = isOwner && lockStatus !== 'locked-permanently' // A lock forced by a moderation action cannot be undone by the owner.
const canLockGroupChat =
isOwner &&
lockStatus !== 'locked-permanently' &&
!convo.details.lockStatusModerationOverride
const groupNameComponent = ( const groupNameComponent = (
<Text <Text
@@ -31,6 +31,7 @@ export function ChatFooter({
style={[ style={[
a.flex_row, a.flex_row,
a.align_center, a.align_center,
a.flex_1,
{ {
minHeight: 32, minHeight: 32,
}, },
@@ -40,7 +41,7 @@ export function ChatFooter({
fill={t.atoms.text_contrast_medium.color} fill={t.atoms.text_contrast_medium.color}
style={[a.mr_sm]} style={[a.mr_sm]}
/> />
<View> <View style={[a.flex_1]}>
<Text <Text
numberOfLines={1} numberOfLines={1}
style={[ style={[
@@ -52,7 +53,7 @@ export function ChatFooter({
</Text> </Text>
{subheading ? ( {subheading ? (
<Text <Text
numberOfLines={1} numberOfLines={2}
style={[ style={[
a.text_xs, a.text_xs,
a.leading_snug, a.leading_snug,
+16 -3
View File
@@ -1,4 +1,5 @@
import {Pressable} from 'react-native' import {Pressable} from 'react-native'
import {ChatBskyConvoUnlockConvo} from '@atproto/api'
import {Trans, useLingui} from '@lingui/react/macro' import {Trans, useLingui} from '@lingui/react/macro'
import {StackActions, useNavigation} from '@react-navigation/native' import {StackActions, useNavigation} from '@react-navigation/native'
@@ -32,12 +33,20 @@ export function ChatLocked({
const primaryMember = convo?.primaryMember const primaryMember = convo?.primaryMember
const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did const isOwner = !!primaryMember && primaryMember.did === currentAccount?.did
// The lock is forced by a moderation action, so the owner cannot undo it.
const isModerationLock = convo.details.lockStatusModerationOverride
const {mutate: lockConvo} = useLockConvo(convo.view.id, { const {mutate: lockConvo} = useLockConvo(convo.view.id, {
onSuccess: () => { onSuccess: () => {
Toast.show(l({message: 'Group chat unlocked', context: 'toast'})) Toast.show(l({message: 'Group chat unlocked', context: 'toast'}))
}, },
onError: e => { onError: e => {
if (e instanceof ChatBskyConvoUnlockConvo.ConvoLockedByModerationError) {
Toast.show(l`This chat is locked by a moderation action`, {
type: 'error',
})
return
}
logger.error('Failed to unlock group chat', {message: e}) logger.error('Failed to unlock group chat', {message: e})
Toast.show(l`Failed to unlock group chat`, {type: 'error'}) Toast.show(l`Failed to unlock group chat`, {type: 'error'})
}, },
@@ -59,10 +68,14 @@ export function ChatLocked({
return ( return (
<ChatFooter <ChatFooter
heading={l`This chat is locked`} heading={l`This chat is locked`}
subheading={l`No one can send messages`} subheading={
isModerationLock
? l`This group is locked by a moderation action on the owner`
: l`No one can send messages`
}
icon={LockIcon}> icon={LockIcon}>
{isOwner ? ( {isOwner ? (
<> isModerationLock ? null : (
<Pressable <Pressable
accessibilityRole="button" accessibilityRole="button"
hitSlop={HITSLOP_10} hitSlop={HITSLOP_10}
@@ -79,7 +92,7 @@ export function ChatLocked({
<Trans>Unlock chat</Trans> <Trans>Unlock chat</Trans>
</Text> </Text>
</Pressable> </Pressable>
</> )
) : ( ) : (
<> <>
<Pressable <Pressable
+5 -1
View File
@@ -1087,7 +1087,10 @@ export class Convo {
this.commit() this.commit()
} }
updateLockStatus(lockStatus: ChatBskyConvoDefs.ConvoLockStatus) { updateLockStatus(
lockStatus: ChatBskyConvoDefs.ConvoLockStatus,
lockStatusModerationOverride: boolean,
) {
if (this.convo?.kind !== 'group') { if (this.convo?.kind !== 'group') {
throw new Error('updateLockStatus can only be called on group convo') throw new Error('updateLockStatus can only be called on group convo')
} }
@@ -1096,6 +1099,7 @@ export class Convo {
kind: { kind: {
...this.convo.details, ...this.convo.details,
lockStatus, lockStatus,
lockStatusModerationOverride,
}, },
}) })
+9 -2
View File
@@ -151,8 +151,15 @@ export function ConvoProvider({
if (data.kind.joinLink !== convo.convo.details.joinLink) { if (data.kind.joinLink !== convo.convo.details.joinLink) {
convo.updateJoinLink(data.kind.joinLink) convo.updateJoinLink(data.kind.joinLink)
} }
if (data.kind.lockStatus !== convo.convo.details.lockStatus) { if (
convo.updateLockStatus(data.kind.lockStatus) data.kind.lockStatus !== convo.convo.details.lockStatus ||
data.kind.lockStatusModerationOverride !==
convo.convo.details.lockStatusModerationOverride
) {
convo.updateLockStatus(
data.kind.lockStatus,
data.kind.lockStatusModerationOverride,
)
} }
} }
if ( if (
@@ -532,12 +532,22 @@ export function ListConvosProviderInner({
} }
return {...convo, rev: log.rev} return {...convo, rev: log.rev}
}) })
// The log event doesn't say whether the lock is forced by a
// moderation override, so refetch to pick up the flag.
void queryClient.invalidateQueries({
queryKey: CONVO_KEY(log.convoId),
})
} else if (ChatBskyConvoDefs.isLogUnlockConvo(log)) { } else if (ChatBskyConvoDefs.isLogUnlockConvo(log)) {
mutateConvoView(log.convoId, convo => { mutateConvoView(log.convoId, convo => {
if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) { if (ChatBskyConvoDefs.isGroupConvo(convo.kind)) {
return { return {
...convo, ...convo,
kind: {...convo.kind, lockStatus: 'unlocked'}, kind: {
...convo.kind,
lockStatus: 'unlocked',
// An unlocked convo cannot be moderation-locked.
lockStatusModerationOverride: false,
},
rev: log.rev, rev: log.rev,
} }
} }