mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at mod-auth 300 lines 8.8 kB view raw
1import React from 'react' 2import {View} from 'react-native' 3import {ComAtprotoLabelDefs, ComAtprotoModerationDefs} from '@atproto/api' 4import {msg, Trans} from '@lingui/macro' 5import {useLingui} from '@lingui/react' 6import {useMutation} from '@tanstack/react-query' 7 8import {useLabelSubject} from '#/lib/moderation' 9import {useLabelInfo} from '#/lib/moderation/useLabelInfo' 10import {makeProfileLink} from '#/lib/routes/links' 11import {sanitizeHandle} from '#/lib/strings/handles' 12import {logger} from '#/logger' 13import {useAgent, useSession} from '#/state/session' 14import * as Toast from '#/view/com/util/Toast' 15import {atoms as a, useBreakpoints, useTheme} from '#/alf' 16import {Button, ButtonIcon, ButtonText} from '#/components/Button' 17import * as Dialog from '#/components/Dialog' 18import {InlineLinkText} from '#/components/Link' 19import {Text} from '#/components/Typography' 20import {Divider} from '../Divider' 21import {Loader} from '../Loader' 22 23export {useDialogControl as useLabelsOnMeDialogControl} from '#/components/Dialog' 24 25export interface LabelsOnMeDialogProps { 26 control: Dialog.DialogOuterProps['control'] 27 labels: ComAtprotoLabelDefs.Label[] 28 type: 'account' | 'content' 29} 30 31export function LabelsOnMeDialog(props: LabelsOnMeDialogProps) { 32 return ( 33 <Dialog.Outer control={props.control}> 34 <Dialog.Handle /> 35 36 <LabelsOnMeDialogInner {...props} /> 37 </Dialog.Outer> 38 ) 39} 40 41function LabelsOnMeDialogInner(props: LabelsOnMeDialogProps) { 42 const {_} = useLingui() 43 const {currentAccount} = useSession() 44 const [appealingLabel, setAppealingLabel] = React.useState< 45 ComAtprotoLabelDefs.Label | undefined 46 >(undefined) 47 const {labels} = props 48 const isAccount = props.type === 'account' 49 const containsSelfLabel = React.useMemo( 50 () => labels.some(l => l.src === currentAccount?.did), 51 [currentAccount?.did, labels], 52 ) 53 54 return ( 55 <Dialog.ScrollableInner 56 label={ 57 isAccount 58 ? _(msg`The following labels were applied to your account.`) 59 : _(msg`The following labels were applied to your content.`) 60 }> 61 {appealingLabel ? ( 62 <AppealForm 63 label={appealingLabel} 64 control={props.control} 65 onPressBack={() => setAppealingLabel(undefined)} 66 /> 67 ) : ( 68 <> 69 <Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}> 70 {isAccount ? ( 71 <Trans>Labels on your account</Trans> 72 ) : ( 73 <Trans>Labels on your content</Trans> 74 )} 75 </Text> 76 <Text style={[a.text_md, a.leading_snug]}> 77 {containsSelfLabel ? ( 78 <Trans> 79 You may appeal non-self labels if you feel they were placed in 80 error. 81 </Trans> 82 ) : ( 83 <Trans> 84 You may appeal these labels if you feel they were placed in 85 error. 86 </Trans> 87 )} 88 </Text> 89 90 <View style={[a.py_lg, a.gap_md]}> 91 {labels.map(label => ( 92 <Label 93 key={`${label.val}-${label.src}`} 94 label={label} 95 isSelfLabel={label.src === currentAccount?.did} 96 control={props.control} 97 onPressAppeal={setAppealingLabel} 98 /> 99 ))} 100 </View> 101 </> 102 )} 103 <Dialog.Close /> 104 </Dialog.ScrollableInner> 105 ) 106} 107 108function Label({ 109 label, 110 isSelfLabel, 111 control, 112 onPressAppeal, 113}: { 114 label: ComAtprotoLabelDefs.Label 115 isSelfLabel: boolean 116 control: Dialog.DialogOuterProps['control'] 117 onPressAppeal: (label: ComAtprotoLabelDefs.Label) => void 118}) { 119 const t = useTheme() 120 const {_} = useLingui() 121 const {labeler, strings} = useLabelInfo(label) 122 const sourceName = labeler 123 ? sanitizeHandle(labeler.creator.handle, '@') 124 : label.src 125 return ( 126 <View 127 style={[ 128 a.border, 129 t.atoms.border_contrast_low, 130 a.rounded_sm, 131 a.overflow_hidden, 132 ]}> 133 <View style={[a.p_md, a.gap_sm, a.flex_row]}> 134 <View style={[a.flex_1, a.gap_xs]}> 135 <Text style={[a.font_bold, a.text_md]}>{strings.name}</Text> 136 <Text style={[t.atoms.text_contrast_medium, a.leading_snug]}> 137 {strings.description} 138 </Text> 139 </View> 140 {!isSelfLabel && ( 141 <View> 142 <Button 143 variant="solid" 144 color="secondary" 145 size="small" 146 label={_(msg`Appeal`)} 147 onPress={() => onPressAppeal(label)}> 148 <ButtonText> 149 <Trans>Appeal</Trans> 150 </ButtonText> 151 </Button> 152 </View> 153 )} 154 </View> 155 156 <Divider /> 157 158 <View style={[a.px_md, a.py_sm, t.atoms.bg_contrast_25]}> 159 <Text style={[t.atoms.text_contrast_medium]}> 160 {isSelfLabel ? ( 161 <Trans>This label was applied by you.</Trans> 162 ) : ( 163 <Trans> 164 Source:{' '} 165 <InlineLinkText 166 label={sourceName} 167 to={makeProfileLink( 168 labeler ? labeler.creator : {did: label.src, handle: ''}, 169 )} 170 onPress={() => control.close()}> 171 {sourceName} 172 </InlineLinkText> 173 </Trans> 174 )} 175 </Text> 176 </View> 177 </View> 178 ) 179} 180 181function AppealForm({ 182 label, 183 control, 184 onPressBack, 185}: { 186 label: ComAtprotoLabelDefs.Label 187 control: Dialog.DialogOuterProps['control'] 188 onPressBack: () => void 189}) { 190 const {_} = useLingui() 191 const {labeler, strings} = useLabelInfo(label) 192 const {gtMobile} = useBreakpoints() 193 const [details, setDetails] = React.useState('') 194 const {subject} = useLabelSubject({label}) 195 const isAccountReport = 'did' in subject 196 const agent = useAgent() 197 const sourceName = labeler 198 ? sanitizeHandle(labeler.creator.handle, '@') 199 : label.src 200 201 const {mutate, isPending} = useMutation({ 202 mutationFn: async () => { 203 const $type = !isAccountReport 204 ? 'com.atproto.repo.strongRef' 205 : 'com.atproto.admin.defs#repoRef' 206 await agent.createModerationReport( 207 { 208 reasonType: ComAtprotoModerationDefs.REASONAPPEAL, 209 subject: { 210 $type, 211 ...subject, 212 }, 213 reason: details, 214 }, 215 { 216 encoding: 'application/json', 217 headers: { 218 'atproto-proxy': `${label.src}#atproto_labeler`, 219 }, 220 }, 221 ) 222 }, 223 onError: err => { 224 logger.error('Failed to submit label appeal', {message: err}) 225 Toast.show(_(msg`Failed to submit appeal, please try again.`), 'xmark') 226 }, 227 onSuccess: () => { 228 control.close() 229 Toast.show(_(msg`Appeal submitted`)) 230 }, 231 }) 232 233 const onSubmit = React.useCallback(() => mutate(), [mutate]) 234 235 return ( 236 <> 237 <Text style={[a.text_2xl, a.font_bold, a.pb_xs, a.leading_tight]}> 238 <Trans>Appeal "{strings.name}" label</Trans> 239 </Text> 240 <Text style={[a.text_md, a.leading_snug]}> 241 <Trans> 242 This appeal will be sent to{' '} 243 <InlineLinkText 244 label={sourceName} 245 to={makeProfileLink( 246 labeler ? labeler.creator : {did: label.src, handle: ''}, 247 )} 248 onPress={() => control.close()} 249 style={[a.text_md, a.leading_snug]}> 250 {sourceName} 251 </InlineLinkText> 252 . 253 </Trans> 254 </Text> 255 <View style={[a.my_md]}> 256 <Dialog.Input 257 label={_(msg`Text input field`)} 258 placeholder={_( 259 msg`Please explain why you think this label was incorrectly applied by ${ 260 labeler ? sanitizeHandle(labeler.creator.handle, '@') : label.src 261 }`, 262 )} 263 value={details} 264 onChangeText={setDetails} 265 autoFocus={true} 266 numberOfLines={3} 267 multiline 268 maxLength={300} 269 /> 270 </View> 271 272 <View 273 style={ 274 gtMobile 275 ? [a.flex_row, a.justify_between] 276 : [{flexDirection: 'column-reverse'}, a.gap_sm] 277 }> 278 <Button 279 testID="backBtn" 280 variant="solid" 281 color="secondary" 282 size="medium" 283 onPress={onPressBack} 284 label={_(msg`Back`)}> 285 <ButtonText>{_(msg`Back`)}</ButtonText> 286 </Button> 287 <Button 288 testID="submitBtn" 289 variant="solid" 290 color="primary" 291 size="medium" 292 onPress={onSubmit} 293 label={_(msg`Submit`)}> 294 <ButtonText>{_(msg`Submit`)}</ButtonText> 295 {isPending && <ButtonIcon icon={Loader} />} 296 </Button> 297 </View> 298 </> 299 ) 300}