Mount message dialogs once at the list level (#10435)
Co-authored-by: Samuel Newman <mozzius@protonmail.com>
This commit is contained in:
@@ -224,6 +224,7 @@ export function Basic({
|
|||||||
cancelButtonCta,
|
cancelButtonCta,
|
||||||
confirmButtonCta,
|
confirmButtonCta,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
|
onClose,
|
||||||
confirmButtonColor,
|
confirmButtonColor,
|
||||||
showCancel = true,
|
showCancel = true,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
@@ -240,11 +241,12 @@ export function Basic({
|
|||||||
* should NOT close the dialog as a side effect of this method.
|
* should NOT close the dialog as a side effect of this method.
|
||||||
*/
|
*/
|
||||||
onConfirm: (e: GestureResponderEvent) => void
|
onConfirm: (e: GestureResponderEvent) => void
|
||||||
|
onClose?: () => void
|
||||||
confirmButtonColor?: ButtonColor
|
confirmButtonColor?: ButtonColor
|
||||||
showCancel?: boolean
|
showCancel?: boolean
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<Outer control={control} testID="confirmModal">
|
<Outer control={control} testID="confirmModal" onClose={onClose}>
|
||||||
<Content>
|
<Content>
|
||||||
<TitleText>{title}</TitleText>
|
<TitleText>{title}</TitleText>
|
||||||
{description && <DescriptionText>{description}</DescriptionText>}
|
{description && <DescriptionText>{description}</DescriptionText>}
|
||||||
|
|||||||
@@ -33,14 +33,19 @@ export const AfterReportDialog = memo(function BlockOrDeleteDialogInner({
|
|||||||
control,
|
control,
|
||||||
params,
|
params,
|
||||||
currentScreen,
|
currentScreen,
|
||||||
|
onClose,
|
||||||
}: {
|
}: {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
params: ReportDialogParams
|
params: ReportDialogParams
|
||||||
currentScreen: 'list' | 'conversation'
|
currentScreen: 'list' | 'conversation'
|
||||||
|
onClose?: () => void
|
||||||
}): React.ReactNode {
|
}): React.ReactNode {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={control} nativeOptions={{preventExpansion: true}}>
|
<Dialog.Outer
|
||||||
|
control={control}
|
||||||
|
onClose={onClose}
|
||||||
|
nativeOptions={{preventExpansion: true}}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
<Dialog.ScrollableInner
|
<Dialog.ScrollableInner
|
||||||
label={l`Would you like to block this user and/or delete this conversation?`}
|
label={l`Would you like to block this user and/or delete this conversation?`}
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import {memo, useCallback} from 'react'
|
import {memo, useCallback} from 'react'
|
||||||
import {LayoutAnimation, Platform} from 'react-native'
|
import {Platform} from 'react-native'
|
||||||
import * as Clipboard from 'expo-clipboard'
|
import * as Clipboard from 'expo-clipboard'
|
||||||
import {
|
import {
|
||||||
type ChatBskyConvoDefs,
|
type ChatBskyConvoDefs,
|
||||||
@@ -7,26 +7,21 @@ import {
|
|||||||
RichText,
|
RichText,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {useLingui} from '@lingui/react/macro'
|
import {useLingui} from '@lingui/react/macro'
|
||||||
import {useQueryClient} from '@tanstack/react-query'
|
|
||||||
|
|
||||||
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
|
import {useGoogleTranslate} from '#/lib/hooks/useGoogleTranslate'
|
||||||
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
import {richTextToString} from '#/lib/strings/rich-text-helpers'
|
||||||
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
||||||
import {useConvoActive} from '#/state/messages/convo'
|
import {useConvoActive} from '#/state/messages/convo'
|
||||||
import {useLanguagePrefs} from '#/state/preferences'
|
import {useLanguagePrefs} from '#/state/preferences'
|
||||||
import {unstableCacheProfileView} from '#/state/queries/unstable-profile-cache'
|
|
||||||
import {useSession} from '#/state/session'
|
import {useSession} from '#/state/session'
|
||||||
import {atoms as a} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import * as ContextMenu from '#/components/ContextMenu'
|
import * as ContextMenu from '#/components/ContextMenu'
|
||||||
import {type TriggerProps} from '#/components/ContextMenu/types'
|
import {type TriggerProps} from '#/components/ContextMenu/types'
|
||||||
import {AfterReportDialog} from '#/components/dms/AfterReportDialog'
|
import {useMessageDialogs} from '#/components/dms/MessageOverlays'
|
||||||
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
import {Clipboard_Stroke2_Corner2_Rounded as ClipboardIcon} from '#/components/icons/Clipboard'
|
||||||
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
|
import {Flag_Stroke2_Corner0_Rounded as FlagIcon} from '#/components/icons/Flag'
|
||||||
import {Language_Stroke2_Corner2_Rounded as LanguageIcon} from '#/components/icons/Language'
|
import {Language_Stroke2_Corner2_Rounded as LanguageIcon} from '#/components/icons/Language'
|
||||||
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
import {Trash_Stroke2_Corner0_Rounded as TrashIcon} from '#/components/icons/Trash'
|
||||||
import {ReportDialog} from '#/components/moderation/ReportDialog'
|
|
||||||
import * as Prompt from '#/components/Prompt'
|
|
||||||
import {usePromptControl} from '#/components/Prompt'
|
|
||||||
import * as Toast from '#/components/Toast'
|
import * as Toast from '#/components/Toast'
|
||||||
import {useAnalytics} from '#/analytics'
|
import {useAnalytics} from '#/analytics'
|
||||||
import {IS_NATIVE} from '#/env'
|
import {IS_NATIVE} from '#/env'
|
||||||
@@ -48,11 +43,8 @@ export let MessageContextMenu = ({
|
|||||||
const {t: l, i18n} = useLingui()
|
const {t: l, i18n} = useLingui()
|
||||||
const ax = useAnalytics()
|
const ax = useAnalytics()
|
||||||
const {currentAccount} = useSession()
|
const {currentAccount} = useSession()
|
||||||
const queryClient = useQueryClient()
|
|
||||||
const convo = useConvoActive()
|
const convo = useConvoActive()
|
||||||
const deleteControl = usePromptControl()
|
const {openDeleteMessage, openReportMessage} = useMessageDialogs()
|
||||||
const reportControl = usePromptControl()
|
|
||||||
const blockOrDeleteControl = usePromptControl()
|
|
||||||
const langPrefs = useLanguagePrefs()
|
const langPrefs = useLanguagePrefs()
|
||||||
const translate = useGoogleTranslate()
|
const translate = useGoogleTranslate()
|
||||||
|
|
||||||
@@ -93,14 +85,6 @@ export let MessageContextMenu = ({
|
|||||||
})
|
})
|
||||||
}, [ax, langPrefs.primaryLanguage, message.text, translate])
|
}, [ax, langPrefs.primaryLanguage, message.text, translate])
|
||||||
|
|
||||||
const onDelete = useCallback(() => {
|
|
||||||
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut)
|
|
||||||
convo
|
|
||||||
.deleteMessage(message.id)
|
|
||||||
.then(() => Toast.show(l({message: 'Message deleted', context: 'toast'})))
|
|
||||||
.catch(() => Toast.show(l`Failed to delete message`))
|
|
||||||
}, [l, convo, message.id])
|
|
||||||
|
|
||||||
const onEmojiSelect = useCallback(
|
const onEmojiSelect = useCallback(
|
||||||
(emoji: string) => {
|
(emoji: string) => {
|
||||||
if (
|
if (
|
||||||
@@ -128,7 +112,6 @@ export let MessageContextMenu = ({
|
|||||||
const sender = senderProfile
|
const sender = senderProfile
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<ContextMenu.Root>
|
<ContextMenu.Root>
|
||||||
{IS_NATIVE && reactionsAvailable && (
|
{IS_NATIVE && reactionsAvailable && (
|
||||||
<ContextMenu.AuxiliaryView
|
<ContextMenu.AuxiliaryView
|
||||||
@@ -179,7 +162,7 @@ export let MessageContextMenu = ({
|
|||||||
destructive
|
destructive
|
||||||
testID="messageDropdownDeleteBtn"
|
testID="messageDropdownDeleteBtn"
|
||||||
label={l`Delete message for me`}
|
label={l`Delete message for me`}
|
||||||
onPress={() => deleteControl.open()}>
|
onPress={() => openDeleteMessage(message)}>
|
||||||
<ContextMenu.ItemIcon icon={TrashIcon} position="left" />
|
<ContextMenu.ItemIcon icon={TrashIcon} position="left" />
|
||||||
<ContextMenu.ItemText>{l`Delete for me`}</ContextMenu.ItemText>
|
<ContextMenu.ItemText>{l`Delete for me`}</ContextMenu.ItemText>
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
@@ -188,44 +171,13 @@ export let MessageContextMenu = ({
|
|||||||
destructive
|
destructive
|
||||||
testID="messageDropdownReportBtn"
|
testID="messageDropdownReportBtn"
|
||||||
label={l`Report message`}
|
label={l`Report message`}
|
||||||
onPress={() => reportControl.open()}>
|
onPress={() => openReportMessage(message, senderProfile)}>
|
||||||
<ContextMenu.ItemIcon icon={FlagIcon} position="left" />
|
<ContextMenu.ItemIcon icon={FlagIcon} position="left" />
|
||||||
<ContextMenu.ItemText>{l`Report`}</ContextMenu.ItemText>
|
<ContextMenu.ItemText>{l`Report`}</ContextMenu.ItemText>
|
||||||
</ContextMenu.Item>
|
</ContextMenu.Item>
|
||||||
)}
|
)}
|
||||||
</ContextMenu.Outer>
|
</ContextMenu.Outer>
|
||||||
</ContextMenu.Root>
|
</ContextMenu.Root>
|
||||||
<ReportDialog
|
|
||||||
control={reportControl}
|
|
||||||
subject={{
|
|
||||||
view: 'message',
|
|
||||||
convoId: convo.convo.view.id,
|
|
||||||
message,
|
|
||||||
}}
|
|
||||||
onAfterSubmit={() => {
|
|
||||||
if (sender) {
|
|
||||||
unstableCacheProfileView(queryClient, sender)
|
|
||||||
}
|
|
||||||
blockOrDeleteControl.open()
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<AfterReportDialog
|
|
||||||
control={blockOrDeleteControl}
|
|
||||||
currentScreen="conversation"
|
|
||||||
params={{
|
|
||||||
convoId: convo.convo.view.id,
|
|
||||||
did: message.sender.did,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Prompt.Basic
|
|
||||||
control={deleteControl}
|
|
||||||
title={l`Delete message`}
|
|
||||||
description={l`Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants.`}
|
|
||||||
confirmButtonCta={l`Delete`}
|
|
||||||
confirmButtonColor="negative"
|
|
||||||
onConfirm={onDelete}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
MessageContextMenu = memo(MessageContextMenu)
|
MessageContextMenu = memo(MessageContextMenu)
|
||||||
|
|||||||
@@ -40,8 +40,8 @@ import {useSession} from '#/state/session'
|
|||||||
import {atoms as a, native, platform, useTheme} from '#/alf'
|
import {atoms as a, native, platform, useTheme} from '#/alf'
|
||||||
import {isOnlyEmoji} from '#/alf/typography'
|
import {isOnlyEmoji} from '#/alf/typography'
|
||||||
import {Button} from '#/components/Button'
|
import {Button} from '#/components/Button'
|
||||||
import {useDialogControl} from '#/components/Dialog'
|
|
||||||
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
|
import {ActionsWrapper} from '#/components/dms/ActionsWrapper'
|
||||||
|
import {useMessageDialogs} from '#/components/dms/MessageOverlays'
|
||||||
import {InlineLinkText, Link} from '#/components/Link'
|
import {InlineLinkText, Link} from '#/components/Link'
|
||||||
import * as ProfileCard from '#/components/ProfileCard'
|
import * as ProfileCard from '#/components/ProfileCard'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
@@ -49,7 +49,7 @@ import {RichText} from '#/components/RichText'
|
|||||||
import {Text} from '#/components/Typography'
|
import {Text} from '#/components/Typography'
|
||||||
import {DateDivider} from './DateDivider'
|
import {DateDivider} from './DateDivider'
|
||||||
import {MessageItemEmbed} from './MessageItemEmbed'
|
import {MessageItemEmbed} from './MessageItemEmbed'
|
||||||
import {ReactionsDialog} from './ReactionsDialog'
|
import {groupReactions} from './ReactionsDialog'
|
||||||
import {CLUSTERED_MESSAGE_THRESHOLD_MS, MESSAGE_GAP_THRESHOLD_MS} from './util'
|
import {CLUSTERED_MESSAGE_THRESHOLD_MS, MESSAGE_GAP_THRESHOLD_MS} from './util'
|
||||||
|
|
||||||
const AVATAR_SIZE = 28
|
const AVATAR_SIZE = 28
|
||||||
@@ -118,7 +118,7 @@ let MessageItem = ({
|
|||||||
const {message} = item
|
const {message} = item
|
||||||
const profile = useMaybeProfileShadow(relatedProfiles.get(message.sender.did))
|
const profile = useMaybeProfileShadow(relatedProfiles.get(message.sender.did))
|
||||||
|
|
||||||
const reactionsControl = useDialogControl()
|
const {openReactions} = useMessageDialogs()
|
||||||
|
|
||||||
const isPending = item.type === 'pending-message'
|
const isPending = item.type === 'pending-message'
|
||||||
|
|
||||||
@@ -243,34 +243,10 @@ let MessageItem = ({
|
|||||||
<ProfileCard.AvatarPlaceholder size={AVATAR_SIZE} />
|
<ProfileCard.AvatarPlaceholder size={AVATAR_SIZE} />
|
||||||
)
|
)
|
||||||
|
|
||||||
const groupedReactions = useMemo(() => {
|
const groupedReactions = useMemo(
|
||||||
const reactions = message.reactions ?? []
|
() => groupReactions(message.reactions),
|
||||||
const grouped = new Map<
|
[message.reactions],
|
||||||
string,
|
)
|
||||||
{
|
|
||||||
key: string
|
|
||||||
value: string
|
|
||||||
senders: ChatBskyConvoDefs.ReactionViewSender[]
|
|
||||||
count: number
|
|
||||||
}
|
|
||||||
>()
|
|
||||||
for (const reaction of reactions) {
|
|
||||||
if (!reaction) continue
|
|
||||||
const existing = grouped.get(reaction.value)
|
|
||||||
if (existing) {
|
|
||||||
existing.senders.push(reaction.sender)
|
|
||||||
existing.count++
|
|
||||||
} else {
|
|
||||||
grouped.set(reaction.value, {
|
|
||||||
key: reaction.value,
|
|
||||||
value: reaction.value,
|
|
||||||
senders: [reaction.sender],
|
|
||||||
count: 1,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return Array.from(grouped.values())
|
|
||||||
}, [message.reactions])
|
|
||||||
|
|
||||||
const reactions = useMemo(() => message.reactions ?? [], [message.reactions])
|
const reactions = useMemo(() => message.reactions ?? [], [message.reactions])
|
||||||
|
|
||||||
@@ -336,7 +312,7 @@ let MessageItem = ({
|
|||||||
transform: [{translateY: -8}],
|
transform: [{translateY: -8}],
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
onPress={isGroupChat ? reactionsControl.open : undefined}>
|
onPress={isGroupChat ? () => openReactions(message) : undefined}>
|
||||||
{groupedReactions.map(group => (
|
{groupedReactions.map(group => (
|
||||||
<Animated.View
|
<Animated.View
|
||||||
entering={native(ZoomIn.springify(200).delay(400))}
|
entering={native(ZoomIn.springify(200).delay(400))}
|
||||||
@@ -377,13 +353,6 @@ let MessageItem = ({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
<ReactionsDialog
|
|
||||||
control={reactionsControl}
|
|
||||||
relatedProfiles={relatedProfiles}
|
|
||||||
message={message}
|
|
||||||
reactions={message.reactions}
|
|
||||||
groupedReactions={groupedReactions}
|
|
||||||
/>
|
|
||||||
</LayoutAnimationConfig>
|
</LayoutAnimationConfig>
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,175 @@
|
|||||||
|
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<MessageDialogsContextType | null>(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<ChatBskyConvoDefs.MessageView | null>(null)
|
||||||
|
const [reportTarget, setReportTarget] = useState<{
|
||||||
|
message: ChatBskyConvoDefs.MessageView
|
||||||
|
senderProfile: bsky.profile.AnyProfileView | undefined
|
||||||
|
} | null>(null)
|
||||||
|
const [afterReportTarget, setAfterReportTarget] =
|
||||||
|
useState<ChatBskyConvoDefs.MessageView | null>(null)
|
||||||
|
const [reactionsTarget, setReactionsTarget] =
|
||||||
|
useState<ChatBskyConvoDefs.MessageView | null>(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<MessageDialogsContextType>(
|
||||||
|
() => ({openDeleteMessage, openReportMessage, openReactions}),
|
||||||
|
[openDeleteMessage, openReportMessage, openReactions],
|
||||||
|
)
|
||||||
|
|
||||||
|
const reportSubject = reportTarget
|
||||||
|
? ({
|
||||||
|
view: 'message',
|
||||||
|
convoId: convo.convo.view.id,
|
||||||
|
message: reportTarget.message,
|
||||||
|
} as const)
|
||||||
|
: undefined
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Context.Provider value={ctx}>
|
||||||
|
{children}
|
||||||
|
<ReportDialog
|
||||||
|
control={reportControl}
|
||||||
|
subject={reportSubject}
|
||||||
|
onAfterSubmit={onAfterReportSubmit}
|
||||||
|
onClose={() => setReportTarget(null)}
|
||||||
|
/>
|
||||||
|
{afterReportTarget && (
|
||||||
|
<AfterReportDialog
|
||||||
|
control={afterReportControl}
|
||||||
|
currentScreen="conversation"
|
||||||
|
params={{
|
||||||
|
convoId: convo.convo.view.id,
|
||||||
|
did: afterReportTarget.sender.did,
|
||||||
|
}}
|
||||||
|
onClose={() => setAfterReportTarget(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{reactionsTarget && (
|
||||||
|
<ReactionsDialog
|
||||||
|
control={reactionsControl}
|
||||||
|
relatedProfiles={convo.relatedProfiles}
|
||||||
|
message={reactionsTarget}
|
||||||
|
onClose={() => setReactionsTarget(null)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<Prompt.Basic
|
||||||
|
control={deleteControl}
|
||||||
|
title={l`Delete message`}
|
||||||
|
description={l`Are you sure you want to delete this message? The message will be deleted for you, but not for the other participants.`}
|
||||||
|
confirmButtonCta={l`Delete`}
|
||||||
|
confirmButtonColor="negative"
|
||||||
|
onConfirm={onConfirmDelete}
|
||||||
|
onClose={() => setDeleteTarget(null)}
|
||||||
|
/>
|
||||||
|
</Context.Provider>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
import {useRef, useState} from 'react'
|
import {useMemo, useRef, useState} from 'react'
|
||||||
import {
|
import {
|
||||||
LayoutAnimation,
|
LayoutAnimation,
|
||||||
Pressable,
|
Pressable,
|
||||||
@@ -37,14 +37,12 @@ export function ReactionsDialog({
|
|||||||
control,
|
control,
|
||||||
relatedProfiles,
|
relatedProfiles,
|
||||||
message,
|
message,
|
||||||
reactions,
|
onClose,
|
||||||
groupedReactions,
|
|
||||||
}: {
|
}: {
|
||||||
control: Dialog.DialogControlProps
|
control: Dialog.DialogControlProps
|
||||||
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
relatedProfiles: Map<string, ChatBskyActorDefs.ProfileViewBasic>
|
||||||
message: ChatBskyConvoDefs.MessageView
|
message: ChatBskyConvoDefs.MessageView
|
||||||
reactions?: ChatBskyConvoDefs.ReactionView[]
|
onClose?: () => void
|
||||||
groupedReactions?: Reaction[]
|
|
||||||
}) {
|
}) {
|
||||||
const {t: l} = useLingui()
|
const {t: l} = useLingui()
|
||||||
|
|
||||||
@@ -54,6 +52,9 @@ export function ReactionsDialog({
|
|||||||
|
|
||||||
const [selected, setSelected] = useState('all')
|
const [selected, setSelected] = useState('all')
|
||||||
|
|
||||||
|
const reactions = message.reactions
|
||||||
|
const groupedReactions = useMemo(() => groupReactions(reactions), [reactions])
|
||||||
|
|
||||||
const filteredReactions = reactions?.filter(
|
const filteredReactions = reactions?.filter(
|
||||||
r => selected === 'all' || r.value === selected,
|
r => selected === 'all' || r.value === selected,
|
||||||
)
|
)
|
||||||
@@ -78,7 +79,10 @@ export function ReactionsDialog({
|
|||||||
return (
|
return (
|
||||||
<Dialog.Outer
|
<Dialog.Outer
|
||||||
control={control}
|
control={control}
|
||||||
onClose={() => setSelected('all')}
|
onClose={() => {
|
||||||
|
setSelected('all')
|
||||||
|
onClose?.()
|
||||||
|
}}
|
||||||
nativeOptions={{
|
nativeOptions={{
|
||||||
preventExpansion: true,
|
preventExpansion: true,
|
||||||
minHeight: screenHeight / 2,
|
minHeight: screenHeight / 2,
|
||||||
@@ -388,3 +392,25 @@ function ReactionTab({
|
|||||||
</Pressable>
|
</Pressable>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function groupReactions(
|
||||||
|
reactions: ChatBskyConvoDefs.ReactionView[] | undefined,
|
||||||
|
): Reaction[] {
|
||||||
|
const grouped = new Map<string, Reaction>()
|
||||||
|
for (const reaction of reactions ?? []) {
|
||||||
|
if (!reaction) continue
|
||||||
|
const existing = grouped.get(reaction.value)
|
||||||
|
if (existing) {
|
||||||
|
existing.senders.push(reaction.sender)
|
||||||
|
existing.count++
|
||||||
|
} else {
|
||||||
|
grouped.set(reaction.value, {
|
||||||
|
key: reaction.value,
|
||||||
|
value: reaction.value,
|
||||||
|
senders: [reaction.sender],
|
||||||
|
count: 1,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return Array.from(grouped.values())
|
||||||
|
}
|
||||||
|
|||||||
@@ -75,9 +75,11 @@ export function ReportDialog(
|
|||||||
() => (props.subject ? parseReportSubject(props.subject) : undefined),
|
() => (props.subject ? parseReportSubject(props.subject) : undefined),
|
||||||
[props.subject],
|
[props.subject],
|
||||||
)
|
)
|
||||||
|
const propsOnClose = props.onClose
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
ax.metric('reportDialog:close', {})
|
ax.metric('reportDialog:close', {})
|
||||||
}, [ax])
|
propsOnClose?.()
|
||||||
|
}, [ax, propsOnClose])
|
||||||
return (
|
return (
|
||||||
<Dialog.Outer control={props.control} onClose={onClose}>
|
<Dialog.Outer control={props.control} onClose={onClose}>
|
||||||
<Dialog.Handle />
|
<Dialog.Handle />
|
||||||
|
|||||||
@@ -88,4 +88,8 @@ export type ReportDialogProps = {
|
|||||||
* Called if the report was successfully submitted.
|
* Called if the report was successfully submitted.
|
||||||
*/
|
*/
|
||||||
onAfterSubmit?: () => void
|
onAfterSubmit?: () => void
|
||||||
|
/**
|
||||||
|
* Called after the dialog finishes closing.
|
||||||
|
*/
|
||||||
|
onClose?: () => void
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -56,6 +56,7 @@ import {MessageListError} from '#/screens/Messages/components/MessageListError'
|
|||||||
import {atoms as a, platform, tokens, useTheme, web} from '#/alf'
|
import {atoms as a, platform, tokens, useTheme, web} from '#/alf'
|
||||||
import {DateDivider} from '#/components/dms/DateDivider'
|
import {DateDivider} from '#/components/dms/DateDivider'
|
||||||
import {MessageItem} from '#/components/dms/MessageItem'
|
import {MessageItem} from '#/components/dms/MessageItem'
|
||||||
|
import {MessageOverlays} from '#/components/dms/MessageOverlays'
|
||||||
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
||||||
import {SystemMessageGroup} from '#/components/dms/SystemMessageGroup'
|
import {SystemMessageGroup} from '#/components/dms/SystemMessageGroup'
|
||||||
import {SystemMessageItem} from '#/components/dms/SystemMessageItem'
|
import {SystemMessageItem} from '#/components/dms/SystemMessageItem'
|
||||||
@@ -498,6 +499,7 @@ export function MessagesList({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<InviteLinkDialogProvider convo={convoState.convo}>
|
<InviteLinkDialogProvider convo={convoState.convo}>
|
||||||
|
<MessageOverlays>
|
||||||
<KeyboardGestureArea
|
<KeyboardGestureArea
|
||||||
interpolator="ios"
|
interpolator="ios"
|
||||||
// HACKFIX: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1419
|
// HACKFIX: https://github.com/kirillzyusko/react-native-keyboard-controller/issues/1419
|
||||||
@@ -585,7 +587,9 @@ export function MessagesList({
|
|||||||
convoState={convoState}
|
convoState={convoState}
|
||||||
hasAcceptOverride={hasAcceptOverride}>
|
hasAcceptOverride={hasAcceptOverride}>
|
||||||
{({loading}) =>
|
{({loading}) =>
|
||||||
ax.features.enabled(ax.features.DmsNewMessageComposerEnable) ? (
|
ax.features.enabled(
|
||||||
|
ax.features.DmsNewMessageComposerEnable,
|
||||||
|
) ? (
|
||||||
<MessageComposer
|
<MessageComposer
|
||||||
textInputId={textInputId}
|
textInputId={textInputId}
|
||||||
onSendMessage={(message: string) =>
|
onSendMessage={(message: string) =>
|
||||||
@@ -618,7 +622,10 @@ export function MessagesList({
|
|||||||
</KeyboardStickyView>
|
</KeyboardStickyView>
|
||||||
</KeyboardGestureArea>
|
</KeyboardGestureArea>
|
||||||
|
|
||||||
{newMessagesPill.show && <NewMessagesPill onPress={scrollToEndOnPress} />}
|
{newMessagesPill.show && (
|
||||||
|
<NewMessagesPill onPress={scrollToEndOnPress} />
|
||||||
|
)}
|
||||||
|
</MessageOverlays>
|
||||||
</InviteLinkDialogProvider>
|
</InviteLinkDialogProvider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user