diff options
author | Samuel Newman <mozzius@protonmail.com> | 2025-01-22 21:03:31 +0000 |
---|---|---|
committer | GitHub <noreply@github.com> | 2025-01-22 21:03:31 +0000 |
commit | 74bb65714b7c7b128ddb27438773b149bbe5ec6c (patch) | |
tree | 83f3348e762938185977b74e51a10f96d4229bc3 | |
parent | 638008c781d4ccb038de57344e18a5237a0f371d (diff) | |
download | voidsky-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>
-rw-r--r-- | src/components/ReportDialog/SubmitView.tsx | 3 | ||||
-rw-r--r-- | src/components/dms/ConvoMenu.tsx | 1 | ||||
-rw-r--r-- | src/components/dms/LeaveConvoPrompt.tsx | 13 | ||||
-rw-r--r-- | src/components/dms/MessageMenu.tsx | 1 | ||||
-rw-r--r-- | src/components/dms/ReportDialog.tsx | 189 | ||||
-rw-r--r-- | src/screens/Messages/ChatList.tsx | 12 | ||||
-rw-r--r-- | src/state/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 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) +} |