From 74bb65714b7c7b128ddb27438773b149bbe5ec6c Mon Sep 17 00:00:00 2001 From: Samuel Newman Date: Wed, 22 Jan 2025 21:03:31 +0000 Subject: [PATCH] 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 --- src/components/ReportDialog/SubmitView.tsx | 3 +- src/components/dms/ConvoMenu.tsx | 1 + src/components/dms/LeaveConvoPrompt.tsx | 13 +- src/components/dms/MessageMenu.tsx | 1 + src/components/dms/ReportDialog.tsx | 189 ++++++++++++++++-- src/screens/Messages/ChatList.tsx | 12 +- .../queries/messages/leave-conversation.ts | 32 ++- 7 files changed, 218 insertions(+), 33 deletions(-) diff --git a/src/components/ReportDialog/SubmitView.tsx b/src/components/ReportDialog/SubmitView.tsx index ef4a9b7fbc..36bd1d4667 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({ Send report - {submitting && } + {/* Maybe fix this later -h */} diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index ba1d4ee543..5b4b68149c 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -227,6 +227,7 @@ let ConvoMenu = ({ /> {latestReportableMessage ? ( () 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 90ee5b9793..fb5474dd10 100644 --- a/src/components/dms/MessageMenu.tsx +++ b/src/components/dms/MessageMenu.tsx @@ -138,6 +138,7 @@ export let MessageMenu = ({ diff --git a/src/components/dms/ReportDialog.tsx b/src/components/dms/ReportDialog.tsx index 06d69ff4be..c9383ff6db 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 ( - + @@ -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(null) + const [done, setDone] = useState(false) + const control = Dialog.useDialogContext() - return reportOption ? ( + return done ? ( + profile ? ( + + ) : ( + + + + ) + ) : reportOption ? ( setReportOption(null)} + onComplete={() => { + if (isError) { + control.close() + } else { + setDone(true) + } + }} /> ) : ( @@ -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({ Send report - {submitting && } + + + + + ) +} + +function DoneStep({ + convoId, + currentScreen, + profile, +}: { + convoId: string + currentScreen: 'list' | 'conversation' + profile: AppBskyActorDefs.ProfileViewBasic +}) { + const {_} = useLingui() + const navigation = useNavigation() + const control = Dialog.useDialogContext() + const {gtMobile} = useBreakpoints() + const t = useTheme() + const [actions, setActions] = useState(['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 ( + + + + Report submitted + + + Our moderation team has recieved your report. + + + + + + + + Block user + + + + + + Delete conversation + + + + + + + + diff --git a/src/screens/Messages/ChatList.tsx b/src/screens/Messages/ChatList.tsx index ac62851123..178e94dd4b 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 faeb926964..21cd1f18ce 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) +}