From 6efe90a5f5c213a02da9f906fc1f098db113d71d Mon Sep 17 00:00:00 2001 From: Eric Bailey Date: Tue, 14 May 2024 20:07:53 -0500 Subject: [PATCH] =?UTF-8?q?[=F0=9F=90=B4]=20Block=20states,=20read=20only?= =?UTF-8?q?=20(#4022)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Refactor ChatListItem for mod state * Refactor Conversation Header for mod state * Invalidate query for list when blocking/unblocking * Remove unused prop, restore border * Add mutations, hook up profile shadow to list query, use shadow-aware query for convo (#4024) --- src/components/dms/ConvoMenu.tsx | 94 +++++++++++++-- src/screens/Messages/Conversation/index.tsx | 112 ++++++++++++------ src/screens/Messages/List/ChatListItem.tsx | 70 ++++++++--- src/screens/Messages/List/index.tsx | 12 +- src/state/cache/profile-shadow.ts | 2 + .../queries/messages/list-converations.ts | 33 +++++- 6 files changed, 250 insertions(+), 73 deletions(-) diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index 0a1d3f01..16e8d98c 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -1,19 +1,27 @@ import React, {useCallback} from 'react' import {Keyboard, Pressable, View} from 'react-native' -import {AppBskyActorDefs, ChatBskyConvoDefs} from '@atproto/api' +import { + AppBskyActorDefs, + ChatBskyConvoDefs, + ModerationDecision, +} from '@atproto/api' import {msg, Trans} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {NavigationProp} from '#/lib/routes/types' +import {listUriToHref} from '#/lib/strings/url-helpers' +import {Shadow} from '#/state/cache/types' import { useConvoQuery, useMarkAsReadMutation, } from '#/state/queries/messages/conversation' import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' import {useMuteConvo} from '#/state/queries/messages/mute-conversation' +import {useProfileBlockMutationQueue} from '#/state/queries/profile' import * as Toast from '#/view/com/util/Toast' import {atoms as a, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft' import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' @@ -22,8 +30,10 @@ import {Person_Stroke2_Corner0_Rounded as Person} from '#/components/icons/Perso import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck' import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX' import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' +import {InlineLinkText} from '#/components/Link' import * as Menu from '#/components/Menu' import * as Prompt from '#/components/Prompt' +import {Text} from '#/components/Typography' import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble' let ConvoMenu = ({ @@ -34,22 +44,35 @@ let ConvoMenu = ({ showMarkAsRead, hideTrigger, triggerOpacity, + moderation, }: { convo: ChatBskyConvoDefs.ConvoView - profile: AppBskyActorDefs.ProfileViewBasic - onUpdateConvo?: (convo: ChatBskyConvoDefs.ConvoView) => void + profile: Shadow control?: Menu.MenuControlProps currentScreen: 'list' | 'conversation' showMarkAsRead?: boolean hideTrigger?: boolean triggerOpacity?: number + moderation: ModerationDecision }): React.ReactNode => { const navigation = useNavigation() const {_} = useLingui() const t = useTheme() const leaveConvoControl = Prompt.usePromptControl() const reportControl = Prompt.usePromptControl() + const blockedByListControl = Prompt.usePromptControl() const {mutate: markAsRead} = useMarkAsReadMutation() + const modui = moderation.ui('profileView') + const {listBlocks, userBlock} = React.useMemo(() => { + const blocks = modui.alerts.filter(alert => alert.type === 'blocking') + const listBlocks = blocks.filter(alert => alert.source.type === 'list') + const userBlock = blocks.find(alert => alert.source.type === 'user') + return { + listBlocks, + userBlock, + } + }, [modui]) + const isBlocking = !!userBlock || !!listBlocks.length const {data: convo} = useConvoQuery(initialConvo) @@ -70,6 +93,21 @@ let ConvoMenu = ({ }, }) + const [queueBlock, queueUnblock] = useProfileBlockMutationQueue(profile) + + const toggleBlock = React.useCallback(() => { + if (listBlocks.length) { + blockedByListControl.open() + return + } + + if (userBlock) { + queueUnblock() + } else { + queueBlock() + } + }, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock]) + const {mutate: leaveConvo} = useLeaveConvo(convo?.id, { onSuccess: () => { if (currentScreen === 'conversation') { @@ -146,18 +184,16 @@ let ConvoMenu = ({ - {/* TODO(samuel): implement this */} {}} - disabled> + label={ + isBlocking ? _(msg`Unblock account`) : _(msg`Block account`) + } + onPress={toggleBlock}> - Block account + {isBlocking ? _(msg`Unblock account`) : _(msg`Block account`)} - + + + + {_(msg`User blocked by list`)} + + + + {_( + msg`This account is blocked by one or more of your moderation lists. To unblock, please visit the lists directly and remove this user.`, + )}{' '} + + + + {_(msg`Lists blocking this user:`)}{' '} + {listBlocks.map((block, i) => + block.source.type === 'list' ? ( + + {i === 0 ? null : ', '} + + {block.source.list.name} + + + ) : null, + )} + + + + + + + + + ) } diff --git a/src/screens/Messages/Conversation/index.tsx b/src/screens/Messages/Conversation/index.tsx index f382647a..05df3e23 100644 --- a/src/screens/Messages/Conversation/index.tsx +++ b/src/screens/Messages/Conversation/index.tsx @@ -3,7 +3,7 @@ import {TouchableOpacity, View} from 'react-native' import {KeyboardProvider} from 'react-native-keyboard-controller' import {KeyboardAvoidingView} from 'react-native-keyboard-controller' import {useSafeAreaInsets} from 'react-native-safe-area-context' -import {AppBskyActorDefs} from '@atproto/api' +import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' @@ -12,8 +12,12 @@ import {NativeStackScreenProps} from '@react-navigation/native-stack' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {useGate} from '#/lib/statsig/statsig' +import {useProfileShadow} from '#/state/cache/profile-shadow' import {useCurrentConvoId} from '#/state/messages/current-convo-id' +import {useModerationOpts} from '#/state/preferences/moderation-opts' +import {useProfileQuery} from '#/state/queries/profile' import {BACK_HITSLOP} from 'lib/constants' +import {sanitizeDisplayName} from 'lib/strings/display-names' import {isIOS, isWeb} from 'platform/detection' import {ConvoProvider, isConvoActive, useConvo} from 'state/messages/convo' import {ConvoStatus} from 'state/messages/convo/types' @@ -27,6 +31,7 @@ import {ListMaybePlaceholder} from '#/components/Lists' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {ClipClopGate} from '../gate' + type Props = NativeStackScreenProps< CommonNavigatorParams, 'MessagesConversation' @@ -137,7 +142,7 @@ function Inner() { } let Header = ({ - profile, + profile: initialProfile, }: { profile?: AppBskyActorDefs.ProfileViewBasic }): React.ReactNode => { @@ -145,12 +150,8 @@ let Header = ({ const {_} = useLingui() const {gtTablet} = useBreakpoints() const navigation = useNavigation() - const convoState = useConvo() - - const isDeletedAccount = profile?.handle === 'missing.invalid' - const displayName = isDeletedAccount - ? 'Deleted Account' - : profile?.displayName + const moderationOpts = useModerationOpts() + const {data: profile} = useProfileQuery({did: initialProfile?.did}) const onPressBack = useCallback(() => { if (isWeb) { @@ -195,23 +196,12 @@ let Header = ({ ) : ( )} - - {profile ? ( - - - - {displayName} - - {!isDeletedAccount && ( - - @{profile.handle} - - )} - - ) : ( - <> + + {profile && moderationOpts ? ( + + ) : ( + <> + - - )} - - {isConvoActive(convoState) && profile ? ( - - ) : ( - + + + + )} ) } Header = React.memo(Header) + +function HeaderReady({ + profile: profileUnshadowed, + moderationOpts, +}: { + profile: AppBskyActorDefs.ProfileViewBasic + moderationOpts: ModerationOpts +}) { + const t = useTheme() + const convoState = useConvo() + const profile = useProfileShadow(profileUnshadowed) + const moderation = React.useMemo( + () => moderateProfile(profile, moderationOpts), + [profile, moderationOpts], + ) + + const isDeletedAccount = profile?.handle === 'missing.invalid' + const displayName = isDeletedAccount + ? 'Deleted Account' + : sanitizeDisplayName( + profile.displayName || profile.handle, + moderation.ui('displayName'), + ) + + return ( + <> + + + + + {displayName} + + {!isDeletedAccount && ( + + @{profile.handle} + + )} + + + + {isConvoActive(convoState) && ( + + )} + + ) +} diff --git a/src/screens/Messages/List/ChatListItem.tsx b/src/screens/Messages/List/ChatListItem.tsx index 57a8e034..aa47e950 100644 --- a/src/screens/Messages/List/ChatListItem.tsx +++ b/src/screens/Messages/List/ChatListItem.tsx @@ -1,13 +1,21 @@ import React from 'react' import {View} from 'react-native' -import {ChatBskyConvoDefs} from '@atproto/api' +import { + AppBskyActorDefs, + ChatBskyConvoDefs, + moderateProfile, + ModerationOpts, +} from '@atproto/api' import {msg} from '@lingui/macro' import {useLingui} from '@lingui/react' import {useNavigation} from '@react-navigation/native' import {NavigationProp} from '#/lib/routes/types' import {isNative} from '#/platform/detection' +import {useProfileShadow} from '#/state/cache/profile-shadow' +import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useSession} from '#/state/session' +import {sanitizeDisplayName} from 'lib/strings/display-names' import {TimeElapsed} from '#/view/com/util/TimeElapsed' import {UserAvatar} from '#/view/com/util/UserAvatar' import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' @@ -17,25 +25,53 @@ import {Bell2Off_Filled_Corner0_Rounded as BellStroke} from '#/components/icons/ import {useMenuControl} from '#/components/Menu' import {Text} from '#/components/Typography' -export function ChatListItem({ +export function ChatListItem({convo}: {convo: ChatBskyConvoDefs.ConvoView}) { + const {currentAccount} = useSession() + const otherUser = convo.members.find( + member => member.did !== currentAccount?.did, + ) + const moderationOpts = useModerationOpts() + + if (!otherUser || !moderationOpts) { + return null + } + + return ( + + ) +} + +function ChatListItemReady({ convo, - index, + profile: profileUnshadowed, + moderationOpts, }: { convo: ChatBskyConvoDefs.ConvoView - index: number + profile: AppBskyActorDefs.ProfileViewBasic + moderationOpts: ModerationOpts }) { const t = useTheme() const {_} = useLingui() const {currentAccount} = useSession() const menuControl = useMenuControl() const {gtMobile} = useBreakpoints() - const otherUser = convo.members.find( - member => member.did !== currentAccount?.did, + const profile = useProfileShadow(profileUnshadowed) + const moderation = React.useMemo( + () => moderateProfile(profile, moderationOpts), + [profile, moderationOpts], ) - const isDeletedAccount = otherUser?.handle === 'missing.invalid' + + const isDeletedAccount = profile.handle === 'missing.invalid' const displayName = isDeletedAccount ? 'Deleted Account' - : otherUser?.displayName || otherUser?.handle + : sanitizeDisplayName( + profile.displayName || profile.handle, + moderation.ui('displayName'), + ) let lastMessage = _(msg`No messages yet`) let lastMessageSentAt: string | null = null @@ -73,10 +109,6 @@ export function ChatListItem({ }) }, [convo.id, navigation]) - if (!otherUser) { - return null - } - return (