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