Bluesky app fork with some witchin' additions 馃挮 witchsky.app
bluesky fork client
120
fork

Configure Feed

Select the types of activity you want to include in your feed.

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