[🐴] Appeal form for disabled DMs (#4126)

* add appeal dialog

* use useMutation for the labels on me dialog

* replace text button with small button
zio/stable
Samuel Newman 2024-05-20 22:23:36 +01:00 committed by GitHub
parent e5aa8c081a
commit d3d2dc8ad4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 158 additions and 17 deletions

View File

@ -1,4 +1,10 @@
import React, {useCallback, useMemo, useRef, useState} from 'react' import React, {
useCallback,
useLayoutEffect,
useMemo,
useRef,
useState,
} from 'react'
import type {TextInput as TextInputType} from 'react-native' import type {TextInput as TextInputType} from 'react-native'
import {View} from 'react-native' import {View} from 'react-native'
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
@ -293,7 +299,7 @@ function SearchablePeopleList({
const control = Dialog.useDialogContext() const control = Dialog.useDialogContext()
const listRef = useRef<BottomSheetFlatListMethods>(null) const listRef = useRef<BottomSheetFlatListMethods>(null)
const {currentAccount} = useSession() const {currentAccount} = useSession()
const inputRef = React.useRef<TextInputType>(null) const inputRef = useRef<TextInputType>(null)
const [searchText, setSearchText] = useState('') const [searchText, setSearchText] = useState('')
@ -306,7 +312,7 @@ function SearchablePeopleList({
limit: 12, limit: 12,
}) })
const items = React.useMemo(() => { const items = useMemo(() => {
let _items: Item[] = [] let _items: Item[] = []
if (isError) { if (isError) {
@ -368,7 +374,7 @@ function SearchablePeopleList({
items.push({type: 'empty', key: 'empty', message: _(msg`No results`)}) items.push({type: 'empty', key: 'empty', message: _(msg`No results`)})
} }
const renderItems = React.useCallback( const renderItems = useCallback(
({item}: {item: Item}) => { ({item}: {item: Item}) => {
switch (item.type) { switch (item.type) {
case 'profile': { case 'profile': {
@ -395,7 +401,7 @@ function SearchablePeopleList({
[moderationOpts, onCreateChat], [moderationOpts, onCreateChat],
) )
React.useLayoutEffect(() => { useLayoutEffect(() => {
if (isWeb) { if (isWeb) {
setImmediate(() => { setImmediate(() => {
inputRef?.current?.focus() inputRef?.current?.focus()

View File

@ -3,19 +3,21 @@ import {View} from 'react-native'
import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api' import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro' import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react' import {useLingui} from '@lingui/react'
import {useMutation} from '@tanstack/react-query'
import {useLabelInfo} from '#/lib/moderation/useLabelInfo' import {useLabelInfo} from '#/lib/moderation/useLabelInfo'
import {makeProfileLink} from '#/lib/routes/links' import {makeProfileLink} from '#/lib/routes/links'
import {sanitizeHandle} from '#/lib/strings/handles' import {sanitizeHandle} from '#/lib/strings/handles'
import {logger} from '#/logger'
import {useAgent, useSession} from '#/state/session' import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast' import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf' import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonText} from '#/components/Button' import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog' import * as Dialog from '#/components/Dialog'
import {InlineLinkText} from '#/components/Link' import {InlineLinkText} from '#/components/Link'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
import {Divider} from '../Divider' import {Divider} from '../Divider'
import {Loader} from '../Loader'
export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog' export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog'
type Subject = type Subject =
@ -100,7 +102,7 @@ function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) {
label={label} label={label}
isSelfLabel={label.src === currentAccount?.did} isSelfLabel={label.src === currentAccount?.did}
control={props.control} control={props.control}
onPressAppeal={label => setAppealingLabel(label)} onPressAppeal={setAppealingLabel}
/> />
))} ))}
</View> </View>
@ -201,8 +203,8 @@ function AppealForm({
const isAccountReport = 'did' in subject const isAccountReport = 'did' in subject
const {getAgent} = useAgent() const {getAgent} = useAgent()
const onSubmit = async () => { const {mutate, isPending} = useMutation({
try { mutationFn: async () => {
const $type = !isAccountReport const $type = !isAccountReport
? 'com.atproto.repo.strongRef' ? 'com.atproto.repo.strongRef'
: 'com.atproto.admin.defs#repoRef' : 'com.atproto.admin.defs#repoRef'
@ -216,11 +218,18 @@ function AppealForm({
}, },
reason: details, reason: details,
}) })
Toast.show(_(msg`Appeal submitted`)) },
} finally { onError: err => {
logger.error('Failed to submit label appeal', {message: err})
Toast.show(_(msg`Failed to submit appeal, please try again.`))
},
onSuccess: () => {
control.close() control.close()
} Toast.show(_(msg`Appeal submitted`))
} },
})
const onSubmit = React.useCallback(() => mutate(), [mutate])
return ( return (
<> <>
@ -281,6 +290,7 @@ function AppealForm({
onPress={onSubmit} onPress={onSubmit}
label={_(msg`Submit`)}> label={_(msg`Submit`)}>
<ButtonText>{_(msg`Submit`)}</ButtonText> <ButtonText>{_(msg`Submit`)}</ButtonText>
{isPending && <ButtonIcon icon={Loader} />}
</Button> </Button>
</View> </View>
</> </>

View File

@ -1,8 +1,17 @@
import React from 'react' import React, {useCallback, useState} from 'react'
import {View} from 'react-native' import {View} from 'react-native'
import {Trans} from '@lingui/macro' import {ComAtprotoModerationDefs} from '@atproto/api'
import {msg, Trans} from '@lingui/macro'
import {useLingui} from '@lingui/react'
import {useMutation} from '@tanstack/react-query'
import {atoms as a, useTheme} from '#/alf' import {logger} from '#/logger'
import {useAgent, useSession} from '#/state/session'
import * as Toast from '#/view/com/util/Toast'
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
import {Button, ButtonIcon, ButtonText} from '#/components/Button'
import * as Dialog from '#/components/Dialog'
import {Loader} from '#/components/Loader'
import {Text} from '#/components/Typography' import {Text} from '#/components/Typography'
export function ChatDisabled() { export function ChatDisabled() {
@ -20,7 +29,123 @@ export function ChatDisabled() {
access to chats on Bluesky. access to chats on Bluesky.
</Trans> </Trans>
</Text> </Text>
<AppealDialog />
</View> </View>
</View> </View>
) )
} }
function AppealDialog() {
const control = Dialog.useDialogControl()
const {_} = useLingui()
return (
<>
<Button
testID="appealDisabledChatBtn"
variant="solid"
color="secondary"
size="small"
onPress={control.open}
label={_(msg`Appeal this decision`)}
style={a.mt_sm}>
<ButtonText>{_(msg`Appeal this decision`)}</ButtonText>
</Button>
<Dialog.Outer control={control}>
<Dialog.Handle />
<DialogInner />
</Dialog.Outer>
</>
)
}
function DialogInner() {
const {_} = useLingui()
const control = Dialog.useDialogContext()
const [details, setDetails] = useState('')
const {gtMobile} = useBreakpoints()
const {getAgent} = useAgent()
const {currentAccount} = useSession()
const {mutate, isPending} = useMutation({
mutationFn: async () => {
if (!currentAccount)
throw new Error('No current account, should be unreachable')
await getAgent().createModerationReport({
reasonType: ComAtprotoModerationDefs.REASONAPPEAL,
subject: {
$type: 'com.atproto.admin.defs#repoRef',
did: currentAccount.did,
},
reason: details,
})
},
onError: err => {
logger.error('Failed to submit chat appeal', {message: err})
Toast.show(_(msg`Failed to submit appeal, please try again.`))
},
onSuccess: () => {
control.close()
Toast.show(_(msg`Appeal submitted`))
},
})
const onSubmit = useCallback(() => mutate(), [mutate])
const onBack = useCallback(() => control.close(), [control])
return (
<Dialog.ScrollableInner label={_(msg`Appeal this decision`)}>
<Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}>
<Trans>Appeal this decision</Trans>
</Text>
<Text style={[a.text_md, a.leading_snug]}>
<Trans>
This appeal will be sent to the Bluesky moderation service.
</Trans>
</Text>
<View style={[a.my_md]}>
<Dialog.Input
label={_(msg`Text input field`)}
placeholder={_(
msg`Please explain why you think your chats were incorrectly disabled`,
)}
value={details}
onChangeText={setDetails}
autoFocus={true}
numberOfLines={3}
multiline
maxLength={300}
/>
</View>
<View
style={
gtMobile
? [a.flex_row, a.justify_between]
: [{flexDirection: 'column-reverse'}, a.gap_sm]
}>
<Button
testID="backBtn"
variant="solid"
color="secondary"
size="medium"
onPress={onBack}
label={_(msg`Back`)}>
<ButtonText>{_(msg`Back`)}</ButtonText>
</Button>
<Button
testID="submitBtn"
variant="solid"
color="primary"
size="medium"
onPress={onSubmit}
label={_(msg`Submit`)}>
<ButtonText>{_(msg`Submit`)}</ButtonText>
{isPending && <ButtonIcon icon={Loader} />}
</Button>
</View>
<Dialog.Close />
</Dialog.ScrollableInner>
)
}