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