diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index 1e492c80d4..2eac8cdb3a 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -1,6 +1,6 @@ import {memo, useCallback} from 'react' import {Keyboard, View} from 'react-native' -import {ChatBskyConvoDefs, type ModerationCause} from '@atproto/api' +import {type ModerationCause} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' @@ -16,13 +16,18 @@ import { unstableCacheProfileView, useProfileBlockMutationQueue, } from '#/state/queries/profile' +import {useSession} from '#/state/session' import {type ViewStyleProp} from '#/alf' import {atoms as a} from '#/alf' import {Button, ButtonIcon} from '#/components/Button' +import {AfterReportConversationDialog} from '#/components/dms/AfterReportConversationDialog' import {AfterReportDialog} from '#/components/dms/AfterReportDialog' import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog' import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' -import {ReportConversationDialog} from '#/components/dms/ReportConversationDialog' +import { + type ConvoWithDetails, + getConvoReportSubject, +} from '#/components/dms/util' import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeftIcon} from '#/components/icons/ArrowBoxLeft' import {Bubble_Stroke2_Corner2_Rounded as BubbleIcon} from '#/components/icons/Bubble' import {DotGrid3x1_Stroke2_Corner0_Rounded as DotsHorizontalIcon} from '#/components/icons/DotGrid' @@ -39,7 +44,6 @@ import {ReportDialog} from '#/components/moderation/ReportDialog' import * as Prompt from '#/components/Prompt' import * as Toast from '#/components/Toast' import type * as bsky from '#/types/bsky' -import {AfterReportConversationDialog} from './AfterReportConversationDialog' let ConvoMenu = ({ convo, @@ -49,10 +53,9 @@ let ConvoMenu = ({ showMarkAsRead, hideTrigger, blockInfo, - latestReportableMessage, style, }: { - convo: ChatBskyConvoDefs.ConvoView + convo: ConvoWithDetails profile: Shadow control?: Menu.MenuControlProps currentScreen: 'list' | 'conversation' @@ -62,20 +65,21 @@ let ConvoMenu = ({ listBlocks: ModerationCause[] userBlock?: ModerationCause } - latestReportableMessage?: ChatBskyConvoDefs.MessageView style?: ViewStyleProp['style'] }): React.ReactNode => { const {t: l} = useLingui() const queryClient = useQueryClient() + const {currentAccount} = useSession() const leaveConvoControl = Prompt.usePromptControl() const reportControl = Prompt.usePromptControl() const blockedByListControl = Prompt.usePromptControl() - const blockOrDeleteControl = Prompt.usePromptControl() - const deleteControl = Prompt.usePromptControl() + const afterReportControl = Prompt.usePromptControl() const {listBlocks} = blockInfo + const reportSubject = getConvoReportSubject(convo, currentAccount?.did) + return ( <> @@ -108,6 +112,7 @@ let ConvoMenu = ({ showMarkAsRead={showMarkAsRead} blockInfo={blockInfo} convo={convo} + canReport={!!reportSubject} leaveConvoControl={leaveConvoControl} reportControl={reportControl} blockedByListControl={blockedByListControl} @@ -116,54 +121,37 @@ let ConvoMenu = ({ - {latestReportableMessage ? ( - <> - { - const sender = convo.members.find( - member => member.did === latestReportableMessage.sender.did, - ) - if (sender) { - unstableCacheProfileView(queryClient, sender) - } - blockOrDeleteControl.open() - }} - /> - - + {reportSubject && ( + { + unstableCacheProfileView(queryClient, profile) + afterReportControl.open() + }} + /> + )} + {convo.kind === 'group' ? ( + ) : ( - <> - - - + )} + canReport: boolean showMarkAsRead?: boolean blockInfo: { listBlocks: ModerationCause[] @@ -201,9 +191,9 @@ function MenuContent({ const {listBlocks, userBlock} = blockInfo const isBlocking = userBlock || !!listBlocks.length const isDeletedAccount = profile.handle === 'missing.invalid' - const isGroupConvo = ChatBskyConvoDefs.isGroupConvo(initialConvo.kind) + const isGroupConvo = initialConvo.kind === 'group' - const convoId = initialConvo.id + const convoId = initialConvo.view.id const {data: convo} = useConvoQuery({convoId}) const onNavigateToProfile = useCallback(() => { @@ -299,15 +289,17 @@ function MenuContent({ )} - - - - Report conversation - - + {canReport && ( + + + + Report conversation + + + )} diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx index f23e7cdfae..745adc62b7 100644 --- a/src/components/dms/MessagesListHeader.tsx +++ b/src/components/dms/MessagesListHeader.tsx @@ -1,10 +1,6 @@ import {useMemo} from 'react' import {View} from 'react-native' -import { - ChatBskyConvoDefs, - moderateProfile, - type ModerationOpts, -} from '@atproto/api' +import {moderateProfile, type ModerationOpts} from '@atproto/api' import {useLingui} from '@lingui/react/macro' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' @@ -12,7 +8,6 @@ import {makeProfileLink} from '#/lib/routes/links' import {sanitizeHandle} from '#/lib/strings/handles' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useModerationOpts} from '#/state/preferences/moderation-opts' -import {useSession} from '#/state/session' import {PreviewableUserAvatar} from '#/view/com/util/UserAvatar' import {useIsWithinSplitView} from '#/screens/Messages/components/splitView/context' import {atoms as a, useTheme, web} from '#/alf' @@ -88,7 +83,6 @@ function ProfileHeaderReady({ }) { const t = useTheme() const {t: l} = useLingui() - const {currentAccount} = useSession() const profile = useProfileShadow(convo.primaryMember) const moderation = moderateProfile(profile, moderationOpts) @@ -110,12 +104,6 @@ function ProfileHeaderReady({ : createSanitizedDisplayName(profile, true, moderation.ui('displayName')) const handle = isDeletedAccount ? null : sanitizeHandle(profile.handle, '@') - const latestReportableMessage = - ChatBskyConvoDefs.isMessageView(convo.view.lastMessage) && - convo.view.lastMessage.sender?.did !== currentAccount?.did - ? convo.view.lastMessage - : undefined - return ( } /> diff --git a/src/components/dms/util.ts b/src/components/dms/util.ts index 7fe5f91dc3..d10b2296fb 100644 --- a/src/components/dms/util.ts +++ b/src/components/dms/util.ts @@ -10,6 +10,7 @@ import {EMOJI_REACTION_LIMIT} from '#/lib/constants' import {logger} from '#/logger' import {type Shadow} from '#/state/cache/profile-shadow' import {type ConvoState, ConvoStatus} from '#/state/messages/convo/types' +import {type ReportSubject} from '#/components/moderation/ReportDialog/types' import * as bsky from '#/types/bsky' export const MESSAGE_GAP_THRESHOLD_MS = 60 * 60 * 1000 @@ -240,3 +241,41 @@ export function parseConvoView( return null } } + +/** + * Resolves the report subject for a conversation-level "Report conversation" + * action (as opposed to reporting an individual message, which always reports + * that message + its sender). + * + * - group: always report the whole convo, targeting the owner. Returns null if + * the owner has left, in which case there is nothing to report against. + * - direct: report the last reportable message if there is one (i.e. the last + * message exists and wasn't sent by us), otherwise report the whole convo + * targeting the other user. + */ +export function getConvoReportSubject( + convo: ConvoWithDetails, + ownDid: string | undefined, +): ReportSubject | null { + if (convo.kind === 'group') { + if (!convo.primaryMember) return null + return {convoId: convo.view.id, did: convo.primaryMember.did} + } + + const lastMessage = convo.view.lastMessage + const reportableMessage = + ChatBskyConvoDefs.isMessageView(lastMessage) && + lastMessage.sender?.did !== ownDid + ? lastMessage + : null + + if (reportableMessage) { + return { + view: 'convo', + convoId: convo.view.id, + message: reportableMessage, + } + } + + return {convoId: convo.view.id, did: convo.primaryMember.did} +} diff --git a/src/screens/Messages/components/ChatListItem.tsx b/src/screens/Messages/components/ChatListItem.tsx index 70390606fc..1c83a6876b 100644 --- a/src/screens/Messages/components/ChatListItem.tsx +++ b/src/screens/Messages/components/ChatListItem.tsx @@ -307,20 +307,13 @@ function BaseChatItem({ isDeletedAccount || (convo.kind === 'group' && convo.details.lockStatus !== 'unlocked') - const { - lastMessage, - LastMessageIcon, - lastMessageSentAt, - latestReportableMessage, - } = useMemo(() => { + const {lastMessage, LastMessageIcon, lastMessageSentAt} = useMemo(() => { let lastMessage = l`No messages yet` let LastMessageIcon: React.ComponentType | null = null let lastMessageSentAt: string | null = null - let latestReportableMessage: ChatBskyConvoDefs.MessageView | undefined - // Deleted message if (ChatBskyConvoDefs.isDeletedMessageView(convo.view.lastMessage)) { lastMessageSentAt = convo.view.lastMessage.sentAt @@ -340,7 +333,6 @@ function BaseChatItem({ if (info) { lastMessage = info.message ?? lastMessage lastMessageSentAt = info.sentAt - latestReportableMessage = info.reportableMessage } } @@ -385,7 +377,6 @@ function BaseChatItem({ lastMessage, LastMessageIcon, lastMessageSentAt, - latestReportableMessage, } }, [l, convo, currentAccount?.did, isDeletedAccount, i18n]) @@ -663,7 +654,7 @@ function BaseChatItem({ {/* TODO: Allow showing menu for groups where the owner has left! */} {showMenu && primaryProfile && ( )} diff --git a/src/screens/Messages/components/ChatStatusInfo.tsx b/src/screens/Messages/components/ChatStatusInfo.tsx index ecb0139e66..e2e9a75faa 100644 --- a/src/screens/Messages/components/ChatStatusInfo.tsx +++ b/src/screens/Messages/components/ChatStatusInfo.tsx @@ -1,7 +1,7 @@ import {useCallback, useMemo} from 'react' import {View} from 'react-native' import {LinearGradient} from 'expo-linear-gradient' -import {ChatBskyConvoDefs, moderateProfile} from '@atproto/api' +import {moderateProfile} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' @@ -33,12 +33,6 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) { // if we ever allow someone other than the owner to invite people, this will need to change const otherUser = convoState.convo.primaryMember - const lastMessage = ChatBskyConvoDefs.isMessageView( - convoState.convo.view.lastMessage, - ) - ? convoState.convo.view.lastMessage - : null - if (!moderationOpts) { return null } @@ -64,9 +58,9 @@ export function ChatStatusInfo({convoState}: {convoState: ActiveConvoStates}) { {otherUser && ( ) : null} & { label?: string icon?: boolean - convo: ChatBskyConvoDefs.ConvoView + convo: ConvoWithDetails profile: ChatBskyActorDefs.ProfileViewBasic showDeleteConvo?: boolean currentScreen: 'list' | 'conversation' }) { const {t: l} = useLingui() + const {currentAccount} = useSession() const shadowedProfile = useProfileShadow(profile) const navigation = useNavigation() const queryClient = useQueryClient() - const {mutate: leaveConvo} = useLeaveConvo(convo.id, { + const {mutate: leaveConvo} = useLeaveConvo(convo.view.id, { onMutate: () => { if (currentScreen === 'conversation') { navigation.dispatch(StackActions.pop()) @@ -110,9 +117,7 @@ export function RejectMenu({ const reportControl = useDialogControl() const blockOrDeleteControl = useDialogControl() - const lastMessage = ChatBskyConvoDefs.isMessageView(convo.lastMessage) - ? convo.lastMessage - : null + const reportSubject = getConvoReportSubject(convo, currentAccount?.did) return ( <> @@ -152,50 +157,46 @@ export function RejectMenu({ - {/* note: last message will almost certainly be defined, since you can't - delete messages for other people and it's impossible for a convo on this - screen to have a message sent by you */} - {lastMessage && ( - - - Report conversation - - - - )} + + + Report conversation + + + - {lastMessage && ( - <> - { - const sender = convo.members.find( - member => member.did === lastMessage.sender.did, - ) - if (sender) { - unstableCacheProfileView(queryClient, sender) - } - blockOrDeleteControl.open() - }} - /> - - + + {reportSubject && ( + { + unstableCacheProfileView(queryClient, profile) + blockOrDeleteControl.open() + }} + /> + )} + {convo.kind === 'group' ? ( + + ) : ( + )} )