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