diff options
Diffstat (limited to 'src/components')
-rw-r--r-- | src/components/Prompt.tsx | 4 | ||||
-rw-r--r-- | src/components/dms/BlockedByListDialog.tsx | 62 | ||||
-rw-r--r-- | src/components/dms/ConvoMenu.tsx | 104 | ||||
-rw-r--r-- | src/components/dms/LeaveConvoPrompt.tsx | 55 | ||||
-rw-r--r-- | src/components/dms/MessageItem.tsx | 2 | ||||
-rw-r--r-- | src/components/dms/MessagesListBlockedFooter.tsx | 131 | ||||
-rw-r--r-- | src/components/dms/MessagesListHeader.tsx | 194 | ||||
-rw-r--r-- | src/components/dms/ReportConversationPrompt.tsx | 27 |
8 files changed, 492 insertions, 87 deletions
diff --git a/src/components/Prompt.tsx b/src/components/Prompt.tsx index 92e848e8e..d05cab5ab 100644 --- a/src/components/Prompt.tsx +++ b/src/components/Prompt.tsx @@ -172,6 +172,7 @@ export function Basic({ confirmButtonCta, onConfirm, confirmButtonColor, + showCancel = true, }: React.PropsWithChildren<{ control: Dialog.DialogOuterProps['control'] title: string @@ -187,6 +188,7 @@ export function Basic({ */ onConfirm: () => void confirmButtonColor?: ButtonColor + showCancel?: boolean }>) { return ( <Outer control={control} testID="confirmModal"> @@ -199,7 +201,7 @@ export function Basic({ color={confirmButtonColor} testID="confirmBtn" /> - <Cancel cta={cancelButtonCta} /> + {showCancel && <Cancel cta={cancelButtonCta} />} </Actions> </Outer> ) diff --git a/src/components/dms/BlockedByListDialog.tsx b/src/components/dms/BlockedByListDialog.tsx new file mode 100644 index 000000000..a27701605 --- /dev/null +++ b/src/components/dms/BlockedByListDialog.tsx @@ -0,0 +1,62 @@ +import React from 'react' +import {View} from 'react-native' +import {ModerationCause} from '@atproto/api' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {listUriToHref} from 'lib/strings/url-helpers' +import {atoms as a, useTheme} from '#/alf' +import * as Dialog from '#/components/Dialog' +import {DialogControlProps} from '#/components/Dialog' +import {InlineLinkText} from '#/components/Link' +import * as Prompt from '#/components/Prompt' +import {Text} from '#/components/Typography' + +export function BlockedByListDialog({ + control, + listBlocks, +}: { + control: DialogControlProps + listBlocks: ModerationCause[] +}) { + const {_} = useLingui() + const t = useTheme() + + return ( + <Prompt.Outer control={control} testID="blockedByListDialog"> + <Prompt.TitleText>{_(msg`User blocked by list`)}</Prompt.TitleText> + + <View style={[a.gap_sm, a.pb_lg]}> + <Text + selectable + style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}> + {_( + msg`This account is blocked by one or more of your moderation lists. To unblock, please visit the lists directly and remove this user.`, + )}{' '} + </Text> + + <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}> + {_(msg`Lists blocking this user:`)}{' '} + {listBlocks.map((block, i) => + block.source.type === 'list' ? ( + <React.Fragment key={block.source.list.uri}> + {i === 0 ? null : ', '} + <InlineLinkText + to={listUriToHref(block.source.list.uri)} + style={[a.text_md, a.leading_snug]}> + {block.source.list.name} + </InlineLinkText> + </React.Fragment> + ) : null, + )} + </Text> + </View> + + <Prompt.Actions> + <Prompt.Action cta={_(msg`I understand`)} onPress={() => {}} /> + </Prompt.Actions> + + <Dialog.Close /> + </Prompt.Outer> + ) +} diff --git a/src/components/dms/ConvoMenu.tsx b/src/components/dms/ConvoMenu.tsx index cf1dbc171..0e5cd12bf 100644 --- a/src/components/dms/ConvoMenu.tsx +++ b/src/components/dms/ConvoMenu.tsx @@ -3,25 +3,25 @@ import {Keyboard, Pressable, View} from 'react-native' import { AppBskyActorDefs, ChatBskyConvoDefs, - ModerationDecision, + ModerationCause, } 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 {BlockedByListDialog} from '#/components/dms/BlockedByListDialog' +import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' +import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt' 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' @@ -30,10 +30,8 @@ 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 = ({ @@ -44,7 +42,7 @@ let ConvoMenu = ({ showMarkAsRead, hideTrigger, triggerOpacity, - moderation, + blockInfo, }: { convo: ChatBskyConvoDefs.ConvoView profile: Shadow<AppBskyActorDefs.ProfileViewBasic> @@ -53,7 +51,10 @@ let ConvoMenu = ({ showMarkAsRead?: boolean hideTrigger?: boolean triggerOpacity?: number - moderation: ModerationDecision + blockInfo: { + listBlocks: ModerationCause[] + userBlock?: ModerationCause + } }): React.ReactNode => { const navigation = useNavigation<NavigationProp>() const {_} = useLingui() @@ -62,17 +63,9 @@ let ConvoMenu = ({ 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 {listBlocks, userBlock} = blockInfo + const isBlocking = userBlock || !!listBlocks.length const {data: convo} = useConvoQuery(initialConvo) @@ -108,17 +101,6 @@ let ConvoMenu = ({ } }, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock]) - const {mutate: leaveConvo} = useLeaveConvo(convo?.id, { - onSuccess: () => { - if (currentScreen === 'conversation') { - navigation.replace('Messages') - } - }, - onError: () => { - Toast.show(_(msg`Could not leave chat`)) - }, - }) - return ( <> <Menu.Root control={control}> @@ -218,67 +200,19 @@ let ConvoMenu = ({ </Menu.Outer> </Menu.Root> - <Prompt.Basic + <LeaveConvoPrompt control={leaveConvoControl} - title={_(msg`Leave conversation`)} - description={_( - msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`, - )} - confirmButtonCta={_(msg`Leave`)} - confirmButtonColor="negative" - onConfirm={() => leaveConvo()} + convoId={convo.id} + currentScreen={currentScreen} /> - - <Prompt.Basic - control={reportControl} - title={_(msg`Report conversation`)} - description={_( - msg`To report a conversation, please report one of its messages via the conversation screen. This lets our moderators understand the context of your issue.`, - )} - confirmButtonCta={_(msg`I understand`)} - onConfirm={noop} + <ReportConversationPrompt control={reportControl} /> + <BlockedByListDialog + control={blockedByListControl} + listBlocks={listBlocks} /> - - <Prompt.Outer control={blockedByListControl} testID="blockedByListDialog"> - <Prompt.TitleText>{_(msg`User blocked by list`)}</Prompt.TitleText> - - <View style={[a.gap_sm, a.pb_lg]}> - <Text - selectable - style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}> - {_( - msg`This account is blocked by one or more of your moderation lists. To unblock, please visit the lists directly and remove this user.`, - )}{' '} - </Text> - - <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}> - {_(msg`Lists blocking this user:`)}{' '} - {listBlocks.map((block, i) => - block.source.type === 'list' ? ( - <React.Fragment key={block.source.list.uri}> - {i === 0 ? null : ', '} - <InlineLinkText - to={listUriToHref(block.source.list.uri)} - style={[a.text_md, a.leading_snug]}> - {block.source.list.name} - </InlineLinkText> - </React.Fragment> - ) : null, - )} - </Text> - </View> - - <Prompt.Actions> - <Prompt.Cancel cta={_(msg`I understand`)} /> - </Prompt.Actions> - - <Dialog.Close /> - </Prompt.Outer> </> ) } ConvoMenu = React.memo(ConvoMenu) export {ConvoMenu} - -function noop() {} diff --git a/src/components/dms/LeaveConvoPrompt.tsx b/src/components/dms/LeaveConvoPrompt.tsx new file mode 100644 index 000000000..1c42dbca0 --- /dev/null +++ b/src/components/dms/LeaveConvoPrompt.tsx @@ -0,0 +1,55 @@ +import React from 'react' +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 {useLeaveConvo} from 'state/queries/messages/leave-conversation' +import * as Toast from 'view/com/util/Toast' +import {DialogOuterProps} from '#/components/Dialog' +import * as Prompt from '#/components/Prompt' + +export function LeaveConvoPrompt({ + control, + convoId, + currentScreen, +}: { + control: DialogOuterProps['control'] + convoId: string + currentScreen: 'list' | 'conversation' +}) { + const {_} = useLingui() + const navigation = useNavigation<NavigationProp>() + + const {mutate: leaveConvo} = useLeaveConvo(convoId, { + onSuccess: () => { + if (currentScreen === 'conversation') { + navigation.replace( + 'Messages', + isNative + ? { + animation: 'pop', + } + : {}, + ) + } + }, + onError: () => { + Toast.show(_(msg`Could not leave chat`)) + }, + }) + + return ( + <Prompt.Basic + control={control} + title={_(msg`Leave conversation`)} + description={_( + msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`, + )} + confirmButtonCta={_(msg`Leave`)} + confirmButtonColor="negative" + onConfirm={leaveConvo} + /> + ) +} diff --git a/src/components/dms/MessageItem.tsx b/src/components/dms/MessageItem.tsx index f456fa474..c5ff81091 100644 --- a/src/components/dms/MessageItem.tsx +++ b/src/components/dms/MessageItem.tsx @@ -75,7 +75,7 @@ let MessageItem = ({ }, [message.text, message.facets]) return ( - <View> + <View style={[isFromSelf ? a.mr_md : a.ml_md]}> <ActionsWrapper isFromSelf={isFromSelf} message={message}> <View style={[ diff --git a/src/components/dms/MessagesListBlockedFooter.tsx b/src/components/dms/MessagesListBlockedFooter.tsx new file mode 100644 index 000000000..a018b8623 --- /dev/null +++ b/src/components/dms/MessagesListBlockedFooter.tsx @@ -0,0 +1,131 @@ +import React from 'react' +import {View} from 'react-native' +import {AppBskyActorDefs, ModerationCause} from '@atproto/api' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {useProfileShadow} from 'state/cache/profile-shadow' +import {useProfileBlockMutationQueue} from 'state/queries/profile' +import {atoms as a, useBreakpoints, useTheme} from '#/alf' +import {Button, ButtonText} from '#/components/Button' +import {useDialogControl} from '#/components/Dialog' +import {Divider} from '#/components/Divider' +import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog' +import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' +import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt' +import {Text} from '#/components/Typography' + +export function MessagesListBlockedFooter({ + recipient: initialRecipient, + convoId, + hasMessages, + blockInfo, +}: { + recipient: AppBskyActorDefs.ProfileViewBasic + convoId: string + hasMessages: boolean + blockInfo: { + listBlocks: ModerationCause[] + userBlock: ModerationCause | undefined + } +}) { + const t = useTheme() + const {gtMobile} = useBreakpoints() + const {_} = useLingui() + const recipient = useProfileShadow(initialRecipient) + const [__, queueUnblock] = useProfileBlockMutationQueue(recipient) + + const leaveConvoControl = useDialogControl() + const reportControl = useDialogControl() + const blockedByListControl = useDialogControl() + + const {listBlocks, userBlock} = blockInfo + const isBlocking = !!userBlock || !!listBlocks.length + + const onUnblockPress = React.useCallback(() => { + if (listBlocks.length) { + blockedByListControl.open() + } else { + queueUnblock() + } + }, [blockedByListControl, listBlocks, queueUnblock]) + + return ( + <View style={[hasMessages && a.pt_md, a.pb_xl, a.gap_lg]}> + <Divider /> + <Text style={[a.text_md, a.font_bold, a.text_center]}> + {isBlocking ? ( + <Trans>You have blocked this user</Trans> + ) : ( + <Trans>This user has blocked you</Trans> + )} + </Text> + + <View style={[a.flex_row, a.justify_between, a.gap_lg, a.px_md]}> + <Button + label={_(msg`Leave chat`)} + color="secondary" + variant="solid" + size="small" + style={[a.flex_1]} + onPress={leaveConvoControl.open}> + <ButtonText style={{color: t.palette.negative_500}}> + <Trans>Leave chat</Trans> + </ButtonText> + </Button> + <Button + label={_(msg`Report`)} + color="secondary" + variant="solid" + size="small" + style={[a.flex_1]} + onPress={reportControl.open}> + <ButtonText style={{color: t.palette.negative_500}}> + <Trans>Report</Trans> + </ButtonText> + </Button> + {isBlocking && gtMobile && ( + <Button + label={_(msg`Unblock`)} + color="secondary" + variant="solid" + size="small" + style={[a.flex_1]} + onPress={onUnblockPress}> + <ButtonText style={{color: t.palette.primary_500}}> + <Trans>Unblock</Trans> + </ButtonText> + </Button> + )} + </View> + {isBlocking && !gtMobile && ( + <View style={[a.flex_row, a.justify_center, a.px_md]}> + <Button + label={_(msg`Unblock`)} + color="secondary" + variant="solid" + size="small" + style={[a.flex_1]} + onPress={onUnblockPress}> + <ButtonText style={{color: t.palette.primary_500}}> + <Trans>Unblock</Trans> + </ButtonText> + </Button> + </View> + )} + + <LeaveConvoPrompt + control={leaveConvoControl} + currentScreen="conversation" + convoId={convoId} + /> + + <ReportConversationPrompt control={reportControl} /> + + <BlockedByListDialog + control={blockedByListControl} + listBlocks={listBlocks} + /> + </View> + ) +} diff --git a/src/components/dms/MessagesListHeader.tsx b/src/components/dms/MessagesListHeader.tsx new file mode 100644 index 000000000..a6dff4032 --- /dev/null +++ b/src/components/dms/MessagesListHeader.tsx @@ -0,0 +1,194 @@ +import React, {useCallback} from 'react' +import {TouchableOpacity, View} from 'react-native' +import { + AppBskyActorDefs, + ModerationCause, + ModerationDecision, +} from '@atproto/api' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' +import {useNavigation} from '@react-navigation/native' + +import {BACK_HITSLOP} from 'lib/constants' +import {makeProfileLink} from 'lib/routes/links' +import {NavigationProp} from 'lib/routes/types' +import {sanitizeDisplayName} from 'lib/strings/display-names' +import {isWeb} from 'platform/detection' +import {useProfileShadow} from 'state/cache/profile-shadow' +import {isConvoActive, useConvo} from 'state/messages/convo' +import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' +import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' +import {ConvoMenu} from '#/components/dms/ConvoMenu' +import {Link} from '#/components/Link' +import {Text} from '#/components/Typography' + +const PFP_SIZE = isWeb ? 40 : 34 + +export let MessagesListHeader = ({ + profile, + moderation, + blockInfo, +}: { + profile?: AppBskyActorDefs.ProfileViewBasic + moderation?: ModerationDecision + blockInfo?: { + listBlocks: ModerationCause[] + userBlock?: ModerationCause + } +}): React.ReactNode => { + const t = useTheme() + const {_} = useLingui() + const {gtTablet} = useBreakpoints() + const navigation = useNavigation<NavigationProp>() + + const onPressBack = useCallback(() => { + if (isWeb) { + navigation.replace('Messages', {}) + } else { + navigation.goBack() + } + }, [navigation]) + + return ( + <View + style={[ + t.atoms.bg, + t.atoms.border_contrast_low, + a.border_b, + a.flex_row, + a.align_center, + a.gap_sm, + gtTablet ? a.pl_lg : a.pl_xl, + a.pr_lg, + a.py_sm, + ]}> + {!gtTablet && ( + <TouchableOpacity + testID="conversationHeaderBackBtn" + onPress={onPressBack} + hitSlop={BACK_HITSLOP} + style={{width: 30, height: 30}} + accessibilityRole="button" + accessibilityLabel={_(msg`Back`)} + accessibilityHint=""> + <FontAwesomeIcon + size={18} + icon="angle-left" + style={{ + marginTop: 6, + }} + color={t.atoms.text.color} + /> + </TouchableOpacity> + )} + + {profile && moderation && blockInfo ? ( + <HeaderReady + profile={profile} + moderation={moderation} + blockInfo={blockInfo} + /> + ) : ( + <> + <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}> + <View + style={[ + {width: PFP_SIZE, height: PFP_SIZE}, + a.rounded_full, + t.atoms.bg_contrast_25, + ]} + /> + <View style={a.gap_xs}> + <View + style={[ + {width: 120, height: 16}, + a.rounded_xs, + t.atoms.bg_contrast_25, + a.mt_xs, + ]} + /> + <View + style={[ + {width: 175, height: 12}, + a.rounded_xs, + t.atoms.bg_contrast_25, + ]} + /> + </View> + </View> + + <View style={{width: 30}} /> + </> + )} + </View> + ) +} +MessagesListHeader = React.memo(MessagesListHeader) + +function HeaderReady({ + profile: profileUnshadowed, + moderation, + blockInfo, +}: { + profile: AppBskyActorDefs.ProfileViewBasic + moderation: ModerationDecision + blockInfo: { + listBlocks: ModerationCause[] + userBlock?: ModerationCause + } +}) { + const t = useTheme() + const convoState = useConvo() + const profile = useProfileShadow(profileUnshadowed) + + const isDeletedAccount = profile?.handle === 'missing.invalid' + const displayName = isDeletedAccount + ? 'Deleted Account' + : sanitizeDisplayName( + profile.displayName || profile.handle, + moderation.ui('displayName'), + ) + + return ( + <> + <Link + style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]} + to={makeProfileLink(profile)}> + <PreviewableUserAvatar + size={PFP_SIZE} + profile={profile} + moderation={moderation.ui('avatar')} + disableHoverCard={moderation.blocked} + /> + <View style={a.flex_1}> + <Text + style={[a.text_md, a.font_bold, web(a.leading_normal)]} + numberOfLines={1}> + {displayName} + </Text> + {!isDeletedAccount && ( + <Text + style={[ + t.atoms.text_contrast_medium, + a.text_sm, + web([a.leading_normal, {marginTop: -2}]), + ]} + numberOfLines={1}> + @{profile.handle} + </Text> + )} + </View> + </Link> + + {isConvoActive(convoState) && ( + <ConvoMenu + convo={convoState.convo} + profile={profile} + currentScreen="conversation" + blockInfo={blockInfo} + /> + )} + </> + ) +} diff --git a/src/components/dms/ReportConversationPrompt.tsx b/src/components/dms/ReportConversationPrompt.tsx new file mode 100644 index 000000000..610cfbcf9 --- /dev/null +++ b/src/components/dms/ReportConversationPrompt.tsx @@ -0,0 +1,27 @@ +import React from 'react' +import {msg} from '@lingui/macro' +import {useLingui} from '@lingui/react' + +import {DialogControlProps} from '#/components/Dialog' +import * as Prompt from '#/components/Prompt' + +export function ReportConversationPrompt({ + control, +}: { + control: DialogControlProps +}) { + const {_} = useLingui() + + return ( + <Prompt.Basic + control={control} + title={_(msg`Report conversation`)} + description={_( + msg`To report a conversation, please report one of its messages via the conversation screen. This lets our moderators understand the context of your issue.`, + )} + confirmButtonCta={_(msg`I understand`)} + onConfirm={() => {}} + showCancel={false} + /> + ) +} |