about summary refs log tree commit diff
path: root/src
diff options
context:
space:
mode:
authorSamuel Newman <mozzius@protonmail.com>2025-01-22 21:03:31 +0000
committerGitHub <noreply@github.com>2025-01-22 21:03:31 +0000
commit74bb65714b7c7b128ddb27438773b149bbe5ec6c (patch)
tree83f3348e762938185977b74e51a10f96d4229bc3 /src
parent638008c781d4ccb038de57344e18a5237a0f371d (diff)
downloadvoidsky-74bb65714b7c7b128ddb27438773b149bbe5ec6c.tar.zst
Post-report menu (#7446)
* post-report block/delete dialog

* fix

* default checked

* web styles

* add icon to send button

* wire everything up

* optimisically leave convo

* hide pending-leave convos

* Capitalize action labels

* Code style

---------

Co-authored-by: Dan Abramov <dan.abramov@gmail.com>
Diffstat (limited to 'src')
-rw-r--r--src/components/ReportDialog/SubmitView.tsx3
-rw-r--r--src/components/dms/ConvoMenu.tsx1
-rw-r--r--src/components/dms/LeaveConvoPrompt.tsx13
-rw-r--r--src/components/dms/MessageMenu.tsx1
-rw-r--r--src/components/dms/ReportDialog.tsx189
-rw-r--r--src/screens/Messages/ChatList.tsx12
-rw-r--r--src/state/queries/messages/leave-conversation.ts32
7 files changed, 218 insertions, 33 deletions
diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx
index ef4a9b7fb..36bd1d466 100644
--- a/src/components/ReportDialog/SubmitView.tsx
+++ b/src/components/ReportDialog/SubmitView.tsx
@@ -16,6 +16,7 @@ import * as Dialog from '#/components/Dialog'
 import * as Toggle from '#/components/forms/Toggle'
 import {Check_Stroke2_Corner0_Rounded as Check} from '#/components/icons/Check'
 import {ChevronLeft_Stroke2_Corner0_Rounded as ChevronLeft} from '#/components/icons/Chevron'
+import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
 import {Loader} from '#/components/Loader'
 import {Text} from '#/components/Typography'
 import {ReportDialogProps} from './types'
@@ -223,7 +224,7 @@ export function SubmitView({
           <ButtonText>
             <Trans>Send report</Trans>
           </ButtonText>
-          {submitting && <ButtonIcon icon={Loader} />}
+          <ButtonIcon icon={submitting ? Loader : SendIcon} />
         </Button>
       </View>
       {/* Maybe fix this later -h */}
diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx
index ba1d4ee54..5b4b68149 100644
--- a/src/components/dms/ConvoMenu.tsx
+++ b/src/components/dms/ConvoMenu.tsx
@@ -227,6 +227,7 @@ let ConvoMenu = ({
       />
       {latestReportableMessage ? (
         <ReportDialog
+          currentScreen={currentScreen}
           params={{
             type: 'convoMessage',
             convoId: convo.id,
diff --git a/src/components/dms/LeaveConvoPrompt.tsx b/src/components/dms/LeaveConvoPrompt.tsx
index cc18c1ab4..c99f8d063 100644
--- a/src/components/dms/LeaveConvoPrompt.tsx
+++ b/src/components/dms/LeaveConvoPrompt.tsx
@@ -1,6 +1,6 @@
 import {msg} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
-import {useNavigation} from '@react-navigation/native'
+import {StackActions, useNavigation} from '@react-navigation/native'
 
 import {NavigationProp} from '#/lib/routes/types'
 import {isNative} from '#/platform/detection'
@@ -22,15 +22,10 @@ export function LeaveConvoPrompt({
   const navigation = useNavigation<NavigationProp>()
 
   const {mutate: leaveConvo} = useLeaveConvo(convoId, {
-    onSuccess: () => {
+    onMutate: () => {
       if (currentScreen === 'conversation') {
-        navigation.replace(
-          'Messages',
-          isNative
-            ? {
-                animation: 'pop',
-              }
-            : {},
+        navigation.dispatch(
+          StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
         )
       }
     },
diff --git a/src/components/dms/MessageMenu.tsx b/src/components/dms/MessageMenu.tsx
index 90ee5b979..fb5474dd1 100644
--- a/src/components/dms/MessageMenu.tsx
+++ b/src/components/dms/MessageMenu.tsx
@@ -138,6 +138,7 @@ export let MessageMenu = ({
       </Menu.Root>
 
       <ReportDialog
+        currentScreen="conversation"
         params={{type: 'convoMessage', convoId: convo.convo.id, message}}
         control={reportControl}
       />
diff --git a/src/components/dms/ReportDialog.tsx b/src/components/dms/ReportDialog.tsx
index 06d69ff4b..c9383ff6d 100644
--- a/src/components/dms/ReportDialog.tsx
+++ b/src/components/dms/ReportDialog.tsx
@@ -1,27 +1,39 @@
 import React, {memo, useMemo, useState} from 'react'
 import {View} from 'react-native'
 import {
+  AppBskyActorDefs,
   ChatBskyConvoDefs,
   ComAtprotoModerationCreateReport,
   RichText as RichTextAPI,
 } from '@atproto/api'
 import {msg, Trans} from '@lingui/macro'
 import {useLingui} from '@lingui/react'
+import {StackActions, useNavigation} from '@react-navigation/native'
 import {useMutation} from '@tanstack/react-query'
 
 import {ReportOption} from '#/lib/moderation/useReportOptions'
+import {NavigationProp} from '#/lib/routes/types'
+import {isNative} from '#/platform/detection'
+import {useProfileShadow} from '#/state/cache/profile-shadow'
+import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
+import {
+  useProfileBlockMutationQueue,
+  useProfileQuery,
+} from '#/state/queries/profile'
 import {useAgent} from '#/state/session'
 import {CharProgress} from '#/view/com/composer/char-progress/CharProgress'
 import * as Toast from '#/view/com/util/Toast'
-import {atoms as a, useBreakpoints, useTheme} from '#/alf'
+import {atoms as a, platform, useBreakpoints, useTheme, web} from '#/alf'
+import {Button, ButtonIcon, ButtonText} from '#/components/Button'
 import * as Dialog from '#/components/Dialog'
-import {Button, ButtonIcon, ButtonText} from '../Button'
-import {Divider} from '../Divider'
-import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '../icons/Chevron'
-import {Loader} from '../Loader'
-import {SelectReportOptionView} from '../ReportDialog/SelectReportOptionView'
-import {RichText} from '../RichText'
-import {Text} from '../Typography'
+import {Divider} from '#/components/Divider'
+import * as Toggle from '#/components/forms/Toggle'
+import {ChevronLeft_Stroke2_Corner0_Rounded as Chevron} from '#/components/icons/Chevron'
+import {PaperPlane_Stroke2_Corner0_Rounded as SendIcon} from '#/components/icons/PaperPlane'
+import {Loader} from '#/components/Loader'
+import {SelectReportOptionView} from '#/components/ReportDialog/SelectReportOptionView'
+import {RichText} from '#/components/RichText'
+import {Text} from '#/components/Typography'
 import {MessageItemMetadata} from './MessageItem'
 
 type ReportDialogParams = {
@@ -33,16 +45,18 @@ type ReportDialogParams = {
 let ReportDialog = ({
   control,
   params,
+  currentScreen,
 }: {
   control: Dialog.DialogControlProps
   params: ReportDialogParams
+  currentScreen: 'list' | 'conversation'
 }): React.ReactNode => {
   const {_} = useLingui()
   return (
     <Dialog.Outer control={control}>
       <Dialog.Handle />
       <Dialog.ScrollableInner label={_(msg`Report this message`)}>
-        <DialogInner params={params} />
+        <DialogInner params={params} currentScreen={currentScreen} />
         <Dialog.Close />
       </Dialog.ScrollableInner>
     </Dialog.Outer>
@@ -51,14 +65,44 @@ let ReportDialog = ({
 ReportDialog = memo(ReportDialog)
 export {ReportDialog}
 
-function DialogInner({params}: {params: ReportDialogParams}) {
+function DialogInner({
+  params,
+  currentScreen,
+}: {
+  params: ReportDialogParams
+  currentScreen: 'list' | 'conversation'
+}) {
+  const {data: profile, isError} = useProfileQuery({
+    did: params.message.sender.did,
+  })
   const [reportOption, setReportOption] = useState<ReportOption | null>(null)
+  const [done, setDone] = useState(false)
+  const control = Dialog.useDialogContext()
 
-  return reportOption ? (
+  return done ? (
+    profile ? (
+      <DoneStep
+        convoId={params.convoId}
+        currentScreen={currentScreen}
+        profile={profile}
+      />
+    ) : (
+      <View style={[a.w_full, a.py_5xl, a.align_center]}>
+        <Loader />
+      </View>
+    )
+  ) : reportOption ? (
     <SubmitStep
       params={params}
       reportOption={reportOption}
       goBack={() => setReportOption(null)}
+      onComplete={() => {
+        if (isError) {
+          control.close()
+        } else {
+          setDone(true)
+        }
+      }}
     />
   ) : (
     <ReasonStep params={params} setReportOption={setReportOption} />
@@ -89,16 +133,17 @@ function SubmitStep({
   params,
   reportOption,
   goBack,
+  onComplete,
 }: {
   params: ReportDialogParams
   reportOption: ReportOption
   goBack: () => void
+  onComplete: () => void
 }) {
   const {_} = useLingui()
   const {gtMobile} = useBreakpoints()
   const t = useTheme()
   const [details, setDetails] = useState('')
-  const control = Dialog.useDialogContext()
   const agent = useAgent()
 
   const {
@@ -124,11 +169,7 @@ function SubmitStep({
         await agent.createModerationReport(report)
       }
     },
-    onSuccess: () => {
-      control.close(() => {
-        Toast.show(_(msg`Thank you. Your report has been sent.`))
-      })
-    },
+    onSuccess: onComplete,
   })
 
   const copy = useMemo(() => {
@@ -181,11 +222,11 @@ function SubmitStep({
         <View style={[a.relative, a.w_full]}>
           <Dialog.Input
             multiline
-            value={details}
+            defaultValue={details}
             onChangeText={setDetails}
             label="Text field"
             style={{paddingRight: 60}}
-            numberOfLines={6}
+            numberOfLines={5}
           />
 
           <View
@@ -231,7 +272,115 @@ function SubmitStep({
           <ButtonText>
             <Trans>Send report</Trans>
           </ButtonText>
-          {submitting && <ButtonIcon icon={Loader} />}
+          <ButtonIcon icon={submitting ? Loader : SendIcon} />
+        </Button>
+      </View>
+    </View>
+  )
+}
+
+function DoneStep({
+  convoId,
+  currentScreen,
+  profile,
+}: {
+  convoId: string
+  currentScreen: 'list' | 'conversation'
+  profile: AppBskyActorDefs.ProfileViewBasic
+}) {
+  const {_} = useLingui()
+  const navigation = useNavigation<NavigationProp>()
+  const control = Dialog.useDialogContext()
+  const {gtMobile} = useBreakpoints()
+  const t = useTheme()
+  const [actions, setActions] = useState<string[]>(['block', 'leave'])
+  const shadow = useProfileShadow(profile)
+  const [queueBlock] = useProfileBlockMutationQueue(shadow)
+
+  const {mutate: leaveConvo} = useLeaveConvo(convoId, {
+    onMutate: () => {
+      if (currentScreen === 'conversation') {
+        navigation.dispatch(
+          StackActions.replace('Messages', isNative ? {animation: 'pop'} : {}),
+        )
+      }
+    },
+    onError: () => {
+      Toast.show(_(msg`Could not leave chat`), 'xmark')
+    },
+  })
+
+  const onPressPrimaryAction = () => {
+    control.close(() => {
+      if (actions.includes('block')) {
+        queueBlock()
+      }
+      if (actions.includes('leave')) {
+        leaveConvo()
+      }
+    })
+  }
+
+  let btnText = _(msg`Done`)
+  if (actions.includes('leave') && actions.includes('block')) {
+    btnText = _(msg`Block and Delete`)
+  } else if (actions.includes('leave')) {
+    btnText = _(msg`Delete Conversation`)
+  } else if (actions.includes('block')) {
+    btnText = _(msg`Block User`)
+  }
+
+  return (
+    <View style={a.gap_2xl}>
+      <View style={[a.justify_center, gtMobile ? a.gap_sm : a.gap_xs]}>
+        <Text style={[a.text_2xl, a.font_bold]}>
+          <Trans>Report submitted</Trans>
+        </Text>
+        <Text style={[a.text_md, t.atoms.text_contrast_medium]}>
+          <Trans>Our moderation team has recieved your report.</Trans>
+        </Text>
+      </View>
+      <Toggle.Group
+        label={_(msg`Block and/or delete this conversation`)}
+        values={actions}
+        onChange={setActions}>
+        <View style={[a.gap_md]}>
+          <Toggle.Item name="block" label={_(msg`Block user`)}>
+            <Toggle.Checkbox />
+            <Toggle.LabelText style={[a.text_md]}>
+              <Trans>Block user</Trans>
+            </Toggle.LabelText>
+          </Toggle.Item>
+          <Toggle.Item name="leave" label={_(msg`Delete coversation`)}>
+            <Toggle.Checkbox />
+            <Toggle.LabelText style={[a.text_md]}>
+              <Trans>Delete conversation</Trans>
+            </Toggle.LabelText>
+          </Toggle.Item>
+        </View>
+      </Toggle.Group>
+
+      <View style={[a.gap_md, web([a.flex_row_reverse])]}>
+        <Button
+          label={btnText}
+          onPress={onPressPrimaryAction}
+          size="large"
+          variant="solid"
+          color={actions.length > 0 ? 'negative' : 'primary'}>
+          <ButtonText>{btnText}</ButtonText>
+        </Button>
+        <Button
+          label={_(msg`Close`)}
+          onPress={() => control.close()}
+          size={platform({native: 'small', web: 'large'})}
+          variant={platform({
+            native: 'ghost',
+            web: 'solid',
+          })}
+          color="secondary">
+          <ButtonText>
+            <Trans>Close</Trans>
+          </ButtonText>
         </Button>
       </View>
     </View>
diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx
index ac6285112..178e94dd4 100644
--- a/src/screens/Messages/ChatList.tsx
+++ b/src/screens/Messages/ChatList.tsx
@@ -14,6 +14,7 @@ import {logger} from '#/logger'
 import {isNative} from '#/platform/detection'
 import {MESSAGE_SCREEN_POLL_INTERVAL} from '#/state/messages/convo/const'
 import {useMessagesEventBus} from '#/state/messages/events'
+import {useLeftConvos} from '#/state/queries/messages/leave-conversation'
 import {useListConvosQuery} from '#/state/queries/messages/list-conversations'
 import {List} from '#/view/com/util/List'
 import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
@@ -94,12 +95,19 @@ export function MessagesScreen({navigation, route}: Props) {
 
   useRefreshOnFocus(refetch)
 
+  const leftConvos = useLeftConvos()
+
   const conversations = useMemo(() => {
     if (data?.pages) {
-      return data.pages.flatMap(page => page.convos)
+      return (
+        data.pages
+          .flatMap(page => page.convos)
+          // filter out convos that are actively being left
+          .filter(convo => !leftConvos.includes(convo.id))
+      )
     }
     return []
-  }, [data])
+  }, [data, leftConvos])
 
   const onRefresh = useCallback(async () => {
     setIsPTRing(true)
diff --git a/src/state/queries/messages/leave-conversation.ts b/src/state/queries/messages/leave-conversation.ts
index faeb92696..21cd1f18c 100644
--- a/src/state/queries/messages/leave-conversation.ts
+++ b/src/state/queries/messages/leave-conversation.ts
@@ -1,17 +1,28 @@
 import {ChatBskyConvoLeaveConvo, ChatBskyConvoListConvos} from '@atproto/api'
-import {useMutation, useQueryClient} from '@tanstack/react-query'
+import {
+  useMutation,
+  useMutationState,
+  useQueryClient,
+} from '@tanstack/react-query'
 
 import {logger} from '#/logger'
 import {DM_SERVICE_HEADERS} from '#/state/queries/messages/const'
 import {useAgent} from '#/state/session'
 import {RQKEY as CONVO_LIST_KEY} from './list-conversations'
 
+const RQKEY_ROOT = 'leave-convo'
+export function RQKEY(convoId: string | undefined) {
+  return [RQKEY_ROOT, convoId]
+}
+
 export function useLeaveConvo(
   convoId: string | undefined,
   {
     onSuccess,
+    onMutate,
     onError,
   }: {
+    onMutate?: () => void
     onSuccess?: (data: ChatBskyConvoLeaveConvo.OutputSchema) => void
     onError?: (error: Error) => void
   },
@@ -20,6 +31,7 @@ export function useLeaveConvo(
   const agent = useAgent()
 
   return useMutation({
+    mutationKey: RQKEY(convoId),
     mutationFn: async () => {
       if (!convoId) throw new Error('No convoId provided')
 
@@ -51,6 +63,7 @@ export function useLeaveConvo(
           }
         },
       )
+      onMutate?.()
       return {prevPages}
     },
     onSuccess: data => {
@@ -77,3 +90,20 @@ export function useLeaveConvo(
     },
   })
 }
+
+/**
+ * Gets currently pending and successful leave convo mutations
+ *
+ * @returns Array of `convoId`
+ */
+export function useLeftConvos() {
+  const pending = useMutationState({
+    filters: {mutationKey: [RQKEY_ROOT], status: 'pending'},
+    select: mutation => mutation.options.mutationKey?.[1] as string | undefined,
+  })
+  const success = useMutationState({
+    filters: {mutationKey: [RQKEY_ROOT], status: 'success'},
+    select: mutation => mutation.options.mutationKey?.[1] as string | undefined,
+  })
+  return [...pending, ...success].filter(id => id !== undefined)
+}