mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
at main 444 lines 12 kB view raw
1import {memo, useMemo, useState} from 'react' 2import {View} from 'react-native' 3import { 4 type $Typed, 5 type AppBskyActorDefs, 6 type ChatBskyConvoDefs, 7 type ComAtprotoModerationCreateReport, 8 RichText as RichTextAPI, 9} from '@atproto/api' 10import {msg, Trans} from '@lingui/macro' 11import {useLingui} from '@lingui/react' 12import {StackActions, useNavigation} from '@react-navigation/native' 13import {useMutation} from '@tanstack/react-query' 14import type React from 'react' 15 16import {BLUESKY_MOD_SERVICE_HEADERS} from '#/lib/constants' 17import {type ReportOption} from '#/lib/moderation/useReportOptions' 18import {type NavigationProp} from '#/lib/routes/types' 19import {isNative} from '#/platform/detection' 20import {useProfileShadow} from '#/state/cache/profile-shadow' 21import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' 22import { 23 useProfileBlockMutationQueue, 24 useProfileQuery, 25} from '#/state/queries/profile' 26import {useAgent} from '#/state/session' 27import {CharProgress} from '#/view/com/composer/char-progress/CharProgress' 28import * as Toast from '#/view/com/util/Toast' 29import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf' 30import {Button, ButtonIcon, ButtonText} from '#/components/Button' 31import * as Dialog from '#/components/Dialog' 32import {Divider} from '#/components/Divider' 33import * as Toggle from '#/components/forms/Toggle' 34import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron' 35import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane' 36import {Loader} from '#/components/Loader' 37import {SelectReportOptionView} from '#/components/ReportDialog/SelectReportOptionView' 38import {RichText} from '#/components/RichText' 39import {Text} from '#/components/Typography' 40import {MessageItemMetadata} from './MessageItem' 41 42type ReportDialogParams = { 43 type: 'convoMessage' 44 convoId: string 45 message: ChatBskyConvoDefs.MessageView 46} 47 48let ReportDialog = ({ 49 control, 50 params, 51 currentScreen, 52}: { 53 control: Dialog.DialogControlProps 54 params: ReportDialogParams 55 currentScreen: 'list' | 'conversation' 56}): React.ReactNode => { 57 const {_} = useLingui() 58 return ( 59 <Dialog.Outer control={control}> 60 <Dialog.Handle /> 61 <Dialog.ScrollableInner label={_(msg`Report this message`)}> 62 <DialogInner params={params} currentScreen={currentScreen} /> 63 <Dialog.Close /> 64 </Dialog.ScrollableInner> 65 </Dialog.Outer> 66 ) 67} 68ReportDialog = memo(ReportDialog) 69export {ReportDialog} 70 71function DialogInner({ 72 params, 73 currentScreen, 74}: { 75 params: ReportDialogParams 76 currentScreen: 'list' | 'conversation' 77}) { 78 const {data: profile, isError} = useProfileQuery({ 79 did: params.message.sender.did, 80 }) 81 const [reportOption, setReportOption] = useState<ReportOption | null>(null) 82 const [done, setDone] = useState(false) 83 const control = Dialog.useDialogContext() 84 85 return done ? ( 86 profile ? ( 87 <DoneStep 88 convoId={params.convoId} 89 currentScreen={currentScreen} 90 profile={profile} 91 /> 92 ) : ( 93 <View style={[a.w_full, a.py_5xl, a.align_center]}> 94 <Loader /> 95 </View> 96 ) 97 ) : reportOption ? ( 98 <SubmitStep 99 params={params} 100 reportOption={reportOption} 101 goBack={() => setReportOption(null)} 102 onComplete={() => { 103 if (isError) { 104 control.close() 105 } else { 106 setDone(true) 107 } 108 }} 109 /> 110 ) : ( 111 <ReasonStep params={params} setReportOption={setReportOption} /> 112 ) 113} 114 115function ReasonStep({ 116 setReportOption, 117}: { 118 setReportOption: (reportOption: ReportOption) => void 119 params: ReportDialogParams 120}) { 121 const control = Dialog.useDialogContext() 122 123 return ( 124 <SelectReportOptionView 125 labelers={[]} 126 goBack={control.close} 127 params={{ 128 type: 'convoMessage', 129 }} 130 onSelectReportOption={setReportOption} 131 /> 132 ) 133} 134 135function SubmitStep({ 136 params, 137 reportOption, 138 goBack, 139 onComplete, 140}: { 141 params: ReportDialogParams 142 reportOption: ReportOption 143 goBack: () => void 144 onComplete: () => void 145}) { 146 const {_} = useLingui() 147 const {gtMobile} = useBreakpoints() 148 const t = useTheme() 149 const [details, setDetails] = useState('') 150 const agent = useAgent() 151 152 const { 153 mutate: submit, 154 error, 155 isPending: submitting, 156 } = useMutation({ 157 mutationFn: async () => { 158 if (params.type === 'convoMessage') { 159 const {convoId, message} = params 160 const subject: $Typed<ChatBskyConvoDefs.MessageRef> = { 161 $type: 'chat.bsky.convo.defs#messageRef', 162 messageId: message.id, 163 convoId, 164 did: message.sender.did, 165 } 166 167 const report = { 168 reasonType: reportOption.reason, 169 subject, 170 reason: details, 171 } satisfies ComAtprotoModerationCreateReport.InputSchema 172 173 await agent.createModerationReport(report, { 174 encoding: 'application/json', 175 headers: BLUESKY_MOD_SERVICE_HEADERS, 176 }) 177 } 178 }, 179 onSuccess: onComplete, 180 }) 181 182 const copy = useMemo(() => { 183 return { 184 convoMessage: { 185 title: _(msg`Report this message`), 186 }, 187 }[params.type] 188 }, [_, params]) 189 190 return ( 191 <View style={a.gap_lg}> 192 <Button 193 size="small" 194 variant="solid" 195 color="secondary" 196 shape="round" 197 label={_(msg`Go back to previous step`)} 198 onPress={goBack}> 199 <ButtonIcon icon={Chevron} /> 200 </Button> 201 202 <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}> 203 <Text style={[a.text_2xl, a.font_semi_bold]}>{copy.title}</Text> 204 <Text style={[a.text_md, t.atoms.text_contrast_medium]}> 205 <Trans> 206 Your report will be sent to the Bluesky Moderation Service 207 </Trans> 208 </Text> 209 </View> 210 211 {params.type === 'convoMessage' && ( 212 <PreviewMessage message={params.message} /> 213 )} 214 215 <Text style={[a.text_md, t.atoms.text_contrast_medium]}> 216 <Text 217 style={[a.font_semi_bold, a.text_md, t.atoms.text_contrast_medium]}> 218 <Trans>Reason:</Trans> 219 </Text>{' '} 220 <Text style={[a.font_semi_bold, a.text_md]}>{reportOption.title}</Text> 221 </Text> 222 223 <Divider /> 224 225 <View style={[a.gap_md]}> 226 <Text style={[t.atoms.text_contrast_medium]}> 227 <Trans>Optionally provide additional information below:</Trans> 228 </Text> 229 230 <View style={[a.relative, a.w_full]}> 231 <Dialog.Input 232 multiline 233 defaultValue={details} 234 onChangeText={setDetails} 235 label={_(msg`Text field`)} 236 style={{paddingRight: 60}} 237 numberOfLines={5} 238 /> 239 <View 240 style={[ 241 a.absolute, 242 a.flex_row, 243 a.align_center, 244 a.pr_md, 245 a.pb_sm, 246 { 247 bottom: 0, 248 right: 0, 249 }, 250 ]}> 251 <CharProgress count={details?.length || 0} /> 252 </View> 253 </View> 254 </View> 255 256 <View style={[a.flex_row, a.align_center, a.justify_end, a.gap_lg]}> 257 {error && ( 258 <Text 259 style={[ 260 a.flex_1, 261 a.italic, 262 a.leading_snug, 263 t.atoms.text_contrast_medium, 264 ]}> 265 <Trans> 266 There was an issue sending your report. Please check your internet 267 connection. 268 </Trans> 269 </Text> 270 )} 271 272 <Button 273 testID="sendReportBtn" 274 size="large" 275 variant="solid" 276 color="negative" 277 label={_(msg`Send report`)} 278 onPress={() => submit()}> 279 <ButtonText> 280 <Trans>Send report</Trans> 281 </ButtonText> 282 <ButtonIcon icon={submitting ? Loader : SendIcon} /> 283 </Button> 284 </View> 285 </View> 286 ) 287} 288 289function DoneStep({ 290 convoId, 291 currentScreen, 292 profile, 293}: { 294 convoId: string 295 currentScreen: 'list' | 'conversation' 296 profile: AppBskyActorDefs.ProfileViewDetailed 297}) { 298 const {_} = useLingui() 299 const navigation = useNavigation<NavigationProp>() 300 const control = Dialog.useDialogContext() 301 const {gtMobile} = useBreakpoints() 302 const t = useTheme() 303 const [actions, setActions] = useState<string[]>(['block', 'leave']) 304 const shadow = useProfileShadow(profile) 305 const [queueBlock] = useProfileBlockMutationQueue(shadow) 306 307 const {mutate: leaveConvo} = useLeaveConvo(convoId, { 308 onMutate: () => { 309 if (currentScreen === 'conversation') { 310 navigation.dispatch( 311 StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}), 312 ) 313 } 314 }, 315 onError: () => { 316 Toast.show(_(msg`Could not leave chat`), 'xmark') 317 }, 318 }) 319 320 let btnText = _(msg`Done`) 321 let toastMsg: string | undefined 322 if (actions.includes('leave') && actions.includes('block')) { 323 btnText = _(msg`Block and Delete`) 324 toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'})) 325 } else if (actions.includes('leave')) { 326 btnText = _(msg`Delete Conversation`) 327 toastMsg = _(msg({message: 'Conversation deleted', context: 'toast'})) 328 } else if (actions.includes('block')) { 329 btnText = _(msg`Block User`) 330 toastMsg = _(msg({message: 'User blocked', context: 'toast'})) 331 } 332 333 const onPressPrimaryAction = () => { 334 control.close(() => { 335 if (actions.includes('block')) { 336 queueBlock() 337 } 338 if (actions.includes('leave')) { 339 leaveConvo() 340 } 341 if (toastMsg) { 342 Toast.show(toastMsg, 'check') 343 } 344 }) 345 } 346 347 return ( 348 <View style={a.gap_2xl}> 349 <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}> 350 <Text style={[a.text_2xl, a.font_semi_bold]}> 351 <Trans>Report submitted</Trans> 352 </Text> 353 <Text style={[a.text_md, t.atoms.text_contrast_medium]}> 354 <Trans>Our moderation team has received your report.</Trans> 355 </Text> 356 </View> 357 <Toggle.Group 358 label={_(msg`Block and/or delete this conversation`)} 359 values={actions} 360 onChange={setActions}> 361 <View style={[a.gap_md]}> 362 <Toggle.Item name="block" label={_(msg`Block user`)}> 363 <Toggle.Checkbox /> 364 <Toggle.LabelText style={[a.text_md]}> 365 <Trans>Block user</Trans> 366 </Toggle.LabelText> 367 </Toggle.Item> 368 <Toggle.Item name="leave" label={_(msg`Delete conversation`)}> 369 <Toggle.Checkbox /> 370 <Toggle.LabelText style={[a.text_md]}> 371 <Trans>Delete conversation</Trans> 372 </Toggle.LabelText> 373 </Toggle.Item> 374 </View> 375 </Toggle.Group> 376 377 <View style={[a.gap_md, web([a.flex_row_reverse])]}> 378 <Button 379 label={btnText} 380 onPress={onPressPrimaryAction} 381 size="large" 382 variant="solid" 383 color={actions.length > 0 ? 'negative' : 'primary'}> 384 <ButtonText>{btnText}</ButtonText> 385 </Button> 386 <Button 387 label={_(msg`Close`)} 388 onPress={() => control.close()} 389 size={platform({native: 'small', web: 'large'})} 390 variant={platform({ 391 native: 'ghost', 392 web: 'solid', 393 })} 394 color="secondary"> 395 <ButtonText> 396 <Trans>Close</Trans> 397 </ButtonText> 398 </Button> 399 </View> 400 </View> 401 ) 402} 403 404function PreviewMessage({message}: {message: ChatBskyConvoDefs.MessageView}) { 405 const t = useTheme() 406 const rt = useMemo(() => { 407 return new RichTextAPI({text: message.text, facets: message.facets}) 408 }, [message.text, message.facets]) 409 410 return ( 411 <View style={a.align_start}> 412 <View 413 style={[ 414 a.py_sm, 415 a.my_2xs, 416 a.rounded_md, 417 { 418 paddingLeft: 14, 419 paddingRight: 14, 420 backgroundColor: t.palette.contrast_50, 421 borderRadius: 17, 422 }, 423 {borderBottomLeftRadius: 2}, 424 ]}> 425 <RichText 426 value={rt} 427 style={[a.text_md, a.leading_snug]} 428 interactiveStyle={a.underline} 429 enableTags 430 /> 431 </View> 432 <MessageItemMetadata 433 item={{ 434 type: 'message', 435 message, 436 key: '', 437 nextMessage: null, 438 prevMessage: null, 439 }} 440 style={[a.text_left, a.mb_0]} 441 /> 442 </View> 443 ) 444}