import { createContext, useCallback, useContext, useEffect, useMemo, useState, } from 'react' import {LayoutAnimation} from 'react-native' import {type ChatBskyConvoDefs} from '@atproto/api' import {useLingui} from '@lingui/react/macro' import {useQueryClient} from '@tanstack/react-query' import {useConvoActive} from '#/state/messages/convo' import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache' import {useDialogControl} from '#/components/Dialog' import {AfterReportDialog} from '#/components/dms/AfterReportDialog' import {ReactionsDialog} from '#/components/dms/ReactionsDialog' import {ReportDialog} from '#/components/moderation/ReportDialog' import * as Prompt from '#/components/Prompt' import {usePromptControl} from '#/components/Prompt' import * as Toast from '#/components/Toast' import type * as bsky from '#/types/bsky' type MessageDialogsContextType = { openDeleteMessage: (message: ChatBskyConvoDefs.MessageView) => void openReportMessage: ( message: ChatBskyConvoDefs.MessageView, senderProfile: bsky.profile.AnyProfileView | undefined, ) => void openReactions: (message: ChatBskyConvoDefs.MessageView) => void } const Context = createContext(null) export function useMessageDialogs() { const ctx = useContext(Context) if (!ctx) { throw new Error('useMessageDialogs must be used within a MessageOverlays') } return ctx } export function MessageOverlays({children}: {children: React.ReactNode}) { const {t: l} = useLingui() const queryClient = useQueryClient() const convo = useConvoActive() const deleteControl = usePromptControl() const reportControl = usePromptControl() const afterReportControl = usePromptControl() const reactionsControl = useDialogControl() const [deleteTarget, setDeleteTarget] = useState(null) const [reportTarget, setReportTarget] = useState<{ message: ChatBskyConvoDefs.MessageView senderProfile: bsky.profile.AnyProfileView | undefined } | null>(null) const [afterReportTarget, setAfterReportTarget] = useState(null) const [reactionsTarget, setReactionsTarget] = useState(null) const openDeleteMessage = useCallback( (message: ChatBskyConvoDefs.MessageView) => { setDeleteTarget(message) deleteControl.open() }, [deleteControl], ) const openReportMessage = useCallback( ( message: ChatBskyConvoDefs.MessageView, senderProfile: bsky.profile.AnyProfileView | undefined, ) => { setReportTarget({message, senderProfile}) reportControl.open() }, [reportControl], ) const openReactions = useCallback( (message: ChatBskyConvoDefs.MessageView) => { setReactionsTarget(message) }, [], ) // These dialogs are conditionally mounted, so we can't open them in the same // tick that we set their targets - the control refs aren't attached yet. Open // in an effect after the dialog has mounted. useEffect(() => { if (reactionsTarget) { reactionsControl.open() } }, [reactionsTarget, reactionsControl]) useEffect(() => { if (afterReportTarget) { afterReportControl.open() } }, [afterReportTarget, afterReportControl]) const onConfirmDelete = useCallback(() => { if (!deleteTarget) return LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut) convo .deleteMessage(deleteTarget.id) .then(() => Toast.show(l({message: 'Message deleted', context: 'toast'}))) .catch(() => Toast.show(l`Failed to delete message`)) }, [l, convo, deleteTarget]) const onAfterReportSubmit = useCallback(() => { if (!reportTarget) return if (reportTarget.senderProfile) { unstableCacheProfileView(queryClient, reportTarget.senderProfile) } setAfterReportTarget(reportTarget.message) }, [queryClient, reportTarget]) const ctx = useMemo( () => ({openDeleteMessage, openReportMessage, openReactions}), [openDeleteMessage, openReportMessage, openReactions], ) // `reactionsTarget` is a snapshot from when the dialog was opened. Read the // live message out of the convo items so optimistic reaction changes (e.g. // "Tap to remove") are reflected in the dialog without closing it first. const reactionsMessage = useMemo(() => { if (!reactionsTarget) return null for (const item of convo.items) { if ( (item.type === 'message' || item.type === 'pending-message') && item.message.id === reactionsTarget.id ) { return item.message } } return reactionsTarget }, [convo.items, reactionsTarget]) const reportSubject = reportTarget ? ({ view: 'message', convoId: convo.convo.view.id, message: reportTarget.message, } as const) : undefined return ( {children} setReportTarget(null)} /> {afterReportTarget && ( setAfterReportTarget(null)} /> )} {reactionsMessage && ( setReactionsTarget(null)} /> )} setDeleteTarget(null)} /> ) }