mirror of https://git.lenooby09.tech/LeNooby09/social-app.git
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

[๐Ÿด] Block Info (#4068)

* get the damn thing in there ๐Ÿ˜ฎโ€๐Ÿ’จ

* more cleanup and little fixes

another nit

nit

small annoyance

add a comment

only use `scrollTo` when necessary

remove now unnecessary styles

* move padding out

* add unblock function

* rm need for moderationpts

* ?

* ??

* extract leaveconvoprompt

* move `setHasScrolled` to `onContentSizeChanged`

* account for block footer

* wrap up

nit

make sure recipient is loaded before showing

refactor to hide chat input

typo squigglie

add report dialog

finalize delete

implement custom animation

add configurable replace animation

add leave convo to block options

* correct functionality for report

* moev component to another file

* maybe...

* fix chat item

* improve

* remove unused gtmobile

* nit

* more cleanup

* more cleanup

* fix merge

* fix header

* few more changes

* nit

* remove old

authored by hailey.at and committed by

GitHub d02e0884 1b47ea73

+599 -280
+4 -1
src/Navigation.tsx
··· 464 464 <MessagesTab.Screen 465 465 name="Messages" 466 466 getComponent={() => MessagesScreen} 467 - options={{requireAuth: true}} 467 + options={({route}) => ({ 468 + requireAuth: true, 469 + animationTypeForReplace: route.params?.animation ?? 'push', 470 + })} 468 471 /> 469 472 {commonScreens(MessagesTab as typeof HomeTab)} 470 473 </MessagesTab.Navigator>
+3 -1
src/components/Prompt.tsx
··· 172 172 confirmButtonCta, 173 173 onConfirm, 174 174 confirmButtonColor, 175 + showCancel = true, 175 176 }: React.PropsWithChildren<{ 176 177 control: Dialog.DialogOuterProps['control'] 177 178 title: string ··· 187 188 */ 188 189 onConfirm: () => void 189 190 confirmButtonColor?: ButtonColor 191 + showCancel?: boolean 190 192 }>) { 191 193 return ( 192 194 <Outer control={control} testID="confirmModal"> ··· 199 201 color={confirmButtonColor} 200 202 testID="confirmBtn" 201 203 /> 202 - <Cancel cta={cancelButtonCta} /> 204 + {showCancel && <Cancel cta={cancelButtonCta} />} 203 205 </Actions> 204 206 </Outer> 205 207 )
+62
src/components/dms/BlockedByListDialog.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {ModerationCause} from '@atproto/api' 4 + import {msg} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + 7 + import {listUriToHref} from 'lib/strings/url-helpers' 8 + import {atoms as a, useTheme} from '#/alf' 9 + import * as Dialog from '#/components/Dialog' 10 + import {DialogControlProps} from '#/components/Dialog' 11 + import {InlineLinkText} from '#/components/Link' 12 + import * as Prompt from '#/components/Prompt' 13 + import {Text} from '#/components/Typography' 14 + 15 + export function BlockedByListDialog({ 16 + control, 17 + listBlocks, 18 + }: { 19 + control: DialogControlProps 20 + listBlocks: ModerationCause[] 21 + }) { 22 + const {_} = useLingui() 23 + const t = useTheme() 24 + 25 + return ( 26 + <Prompt.Outer control={control} testID="blockedByListDialog"> 27 + <Prompt.TitleText>{_(msg`User blocked by list`)}</Prompt.TitleText> 28 + 29 + <View style={[a.gap_sm, a.pb_lg]}> 30 + <Text 31 + selectable 32 + style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}> 33 + {_( 34 + msg`This account is blocked by one or more of your moderation lists. To unblock, please visit the lists directly and remove this user.`, 35 + )}{' '} 36 + </Text> 37 + 38 + <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}> 39 + {_(msg`Lists blocking this user:`)}{' '} 40 + {listBlocks.map((block, i) => 41 + block.source.type === 'list' ? ( 42 + <React.Fragment key={block.source.list.uri}> 43 + {i === 0 ? null : ', '} 44 + <InlineLinkText 45 + to={listUriToHref(block.source.list.uri)} 46 + style={[a.text_md, a.leading_snug]}> 47 + {block.source.list.name} 48 + </InlineLinkText> 49 + </React.Fragment> 50 + ) : null, 51 + )} 52 + </Text> 53 + </View> 54 + 55 + <Prompt.Actions> 56 + <Prompt.Action cta={_(msg`I understand`)} onPress={() => {}} /> 57 + </Prompt.Actions> 58 + 59 + <Dialog.Close /> 60 + </Prompt.Outer> 61 + ) 62 + }
+19 -85
src/components/dms/ConvoMenu.tsx
··· 3 3 import { 4 4 AppBskyActorDefs, 5 5 ChatBskyConvoDefs, 6 - ModerationDecision, 6 + ModerationCause, 7 7 } from '@atproto/api' 8 8 import {msg, Trans} from '@lingui/macro' 9 9 import {useLingui} from '@lingui/react' 10 10 import {useNavigation} from '@react-navigation/native' 11 11 12 12 import {NavigationProp} from '#/lib/routes/types' 13 - import {listUriToHref} from '#/lib/strings/url-helpers' 14 13 import {Shadow} from '#/state/cache/types' 15 14 import { 16 15 useConvoQuery, 17 16 useMarkAsReadMutation, 18 17 } from '#/state/queries/messages/conversation' 19 - import {useLeaveConvo} from '#/state/queries/messages/leave-conversation' 20 18 import {useMuteConvo} from '#/state/queries/messages/mute-conversation' 21 19 import {useProfileBlockMutationQueue} from '#/state/queries/profile' 22 20 import * as Toast from '#/view/com/util/Toast' 23 21 import {atoms as a, useTheme} from '#/alf' 24 - import * as Dialog from '#/components/Dialog' 22 + import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog' 23 + import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' 24 + import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt' 25 25 import {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft' 26 26 import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid' 27 27 import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag' ··· 30 30 import {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck' 31 31 import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX' 32 32 import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker' 33 - import {InlineLinkText} from '#/components/Link' 34 33 import * as Menu from '#/components/Menu' 35 34 import * as Prompt from '#/components/Prompt' 36 - import {Text} from '#/components/Typography' 37 35 import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble' 38 36 39 37 let ConvoMenu = ({ ··· 44 42 showMarkAsRead, 45 43 hideTrigger, 46 44 triggerOpacity, 47 - moderation, 45 + blockInfo, 48 46 }: { 49 47 convo: ChatBskyConvoDefs.ConvoView 50 48 profile: Shadow<AppBskyActorDefs.ProfileViewBasic> ··· 53 51 showMarkAsRead?: boolean 54 52 hideTrigger?: boolean 55 53 triggerOpacity?: number 56 - moderation: ModerationDecision 54 + blockInfo: { 55 + listBlocks: ModerationCause[] 56 + userBlock?: ModerationCause 57 + } 57 58 }): React.ReactNode => { 58 59 const navigation = useNavigation<NavigationProp>() 59 60 const {_} = useLingui() ··· 62 63 const reportControl = Prompt.usePromptControl() 63 64 const blockedByListControl = Prompt.usePromptControl() 64 65 const {mutate: markAsRead} = useMarkAsReadMutation() 65 - const modui = moderation.ui('profileView') 66 - const {listBlocks, userBlock} = React.useMemo(() => { 67 - const blocks = modui.alerts.filter(alert => alert.type === 'blocking') 68 - const listBlocks = blocks.filter(alert => alert.source.type === 'list') 69 - const userBlock = blocks.find(alert => alert.source.type === 'user') 70 - return { 71 - listBlocks, 72 - userBlock, 73 - } 74 - }, [modui]) 75 - const isBlocking = !!userBlock || !!listBlocks.length 66 + 67 + const {listBlocks, userBlock} = blockInfo 68 + const isBlocking = userBlock || !!listBlocks.length 76 69 77 70 const {data: convo} = useConvoQuery(initialConvo) 78 71 ··· 107 100 queueBlock() 108 101 } 109 102 }, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock]) 110 - 111 - const {mutate: leaveConvo} = useLeaveConvo(convo?.id, { 112 - onSuccess: () => { 113 - if (currentScreen === 'conversation') { 114 - navigation.replace('Messages') 115 - } 116 - }, 117 - onError: () => { 118 - Toast.show(_(msg`Could not leave chat`)) 119 - }, 120 - }) 121 103 122 104 return ( 123 105 <> ··· 218 200 </Menu.Outer> 219 201 </Menu.Root> 220 202 221 - <Prompt.Basic 203 + <LeaveConvoPrompt 222 204 control={leaveConvoControl} 223 - title={_(msg`Leave conversation`)} 224 - description={_( 225 - msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`, 226 - )} 227 - confirmButtonCta={_(msg`Leave`)} 228 - confirmButtonColor="negative" 229 - onConfirm={() => leaveConvo()} 205 + convoId={convo.id} 206 + currentScreen={currentScreen} 230 207 /> 231 - 232 - <Prompt.Basic 233 - control={reportControl} 234 - title={_(msg`Report conversation`)} 235 - description={_( 236 - 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.`, 237 - )} 238 - confirmButtonCta={_(msg`I understand`)} 239 - onConfirm={noop} 208 + <ReportConversationPrompt control={reportControl} /> 209 + <BlockedByListDialog 210 + control={blockedByListControl} 211 + listBlocks={listBlocks} 240 212 /> 241 - 242 - <Prompt.Outer control={blockedByListControl} testID="blockedByListDialog"> 243 - <Prompt.TitleText>{_(msg`User blocked by list`)}</Prompt.TitleText> 244 - 245 - <View style={[a.gap_sm, a.pb_lg]}> 246 - <Text 247 - selectable 248 - style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}> 249 - {_( 250 - msg`This account is blocked by one or more of your moderation lists. To unblock, please visit the lists directly and remove this user.`, 251 - )}{' '} 252 - </Text> 253 - 254 - <Text style={[a.text_md, a.leading_snug, t.atoms.text_contrast_high]}> 255 - {_(msg`Lists blocking this user:`)}{' '} 256 - {listBlocks.map((block, i) => 257 - block.source.type === 'list' ? ( 258 - <React.Fragment key={block.source.list.uri}> 259 - {i === 0 ? null : ', '} 260 - <InlineLinkText 261 - to={listUriToHref(block.source.list.uri)} 262 - style={[a.text_md, a.leading_snug]}> 263 - {block.source.list.name} 264 - </InlineLinkText> 265 - </React.Fragment> 266 - ) : null, 267 - )} 268 - </Text> 269 - </View> 270 - 271 - <Prompt.Actions> 272 - <Prompt.Cancel cta={_(msg`I understand`)} /> 273 - </Prompt.Actions> 274 - 275 - <Dialog.Close /> 276 - </Prompt.Outer> 277 213 </> 278 214 ) 279 215 } 280 216 ConvoMenu = React.memo(ConvoMenu) 281 217 282 218 export {ConvoMenu} 283 - 284 - function noop() {}
+55
src/components/dms/LeaveConvoPrompt.tsx
··· 1 + import React from 'react' 2 + import {msg} from '@lingui/macro' 3 + import {useLingui} from '@lingui/react' 4 + import {useNavigation} from '@react-navigation/native' 5 + 6 + import {NavigationProp} from 'lib/routes/types' 7 + import {isNative} from 'platform/detection' 8 + import {useLeaveConvo} from 'state/queries/messages/leave-conversation' 9 + import * as Toast from 'view/com/util/Toast' 10 + import {DialogOuterProps} from '#/components/Dialog' 11 + import * as Prompt from '#/components/Prompt' 12 + 13 + export function LeaveConvoPrompt({ 14 + control, 15 + convoId, 16 + currentScreen, 17 + }: { 18 + control: DialogOuterProps['control'] 19 + convoId: string 20 + currentScreen: 'list' | 'conversation' 21 + }) { 22 + const {_} = useLingui() 23 + const navigation = useNavigation<NavigationProp>() 24 + 25 + const {mutate: leaveConvo} = useLeaveConvo(convoId, { 26 + onSuccess: () => { 27 + if (currentScreen === 'conversation') { 28 + navigation.replace( 29 + 'Messages', 30 + isNative 31 + ? { 32 + animation: 'pop', 33 + } 34 + : {}, 35 + ) 36 + } 37 + }, 38 + onError: () => { 39 + Toast.show(_(msg`Could not leave chat`)) 40 + }, 41 + }) 42 + 43 + return ( 44 + <Prompt.Basic 45 + control={control} 46 + title={_(msg`Leave conversation`)} 47 + description={_( 48 + msg`Are you sure you want to leave this conversation? Your messages will be deleted for you, but not for the other participant.`, 49 + )} 50 + confirmButtonCta={_(msg`Leave`)} 51 + confirmButtonColor="negative" 52 + onConfirm={leaveConvo} 53 + /> 54 + ) 55 + }
+1 -1
src/components/dms/MessageItem.tsx
··· 75 75 }, [message.text, message.facets]) 76 76 77 77 return ( 78 - <View> 78 + <View style={[isFromSelf ? a.mr_md : a.ml_md]}> 79 79 <ActionsWrapper isFromSelf={isFromSelf} message={message}> 80 80 <View 81 81 style={[
+131
src/components/dms/MessagesListBlockedFooter.tsx
··· 1 + import React from 'react' 2 + import {View} from 'react-native' 3 + import {AppBskyActorDefs, ModerationCause} from '@atproto/api' 4 + import {msg, Trans} from '@lingui/macro' 5 + import {useLingui} from '@lingui/react' 6 + 7 + import {useProfileShadow} from 'state/cache/profile-shadow' 8 + import {useProfileBlockMutationQueue} from 'state/queries/profile' 9 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 10 + import {Button, ButtonText} from '#/components/Button' 11 + import {useDialogControl} from '#/components/Dialog' 12 + import {Divider} from '#/components/Divider' 13 + import {BlockedByListDialog} from '#/components/dms/BlockedByListDialog' 14 + import {LeaveConvoPrompt} from '#/components/dms/LeaveConvoPrompt' 15 + import {ReportConversationPrompt} from '#/components/dms/ReportConversationPrompt' 16 + import {Text} from '#/components/Typography' 17 + 18 + export function MessagesListBlockedFooter({ 19 + recipient: initialRecipient, 20 + convoId, 21 + hasMessages, 22 + blockInfo, 23 + }: { 24 + recipient: AppBskyActorDefs.ProfileViewBasic 25 + convoId: string 26 + hasMessages: boolean 27 + blockInfo: { 28 + listBlocks: ModerationCause[] 29 + userBlock: ModerationCause | undefined 30 + } 31 + }) { 32 + const t = useTheme() 33 + const {gtMobile} = useBreakpoints() 34 + const {_} = useLingui() 35 + const recipient = useProfileShadow(initialRecipient) 36 + const [__, queueUnblock] = useProfileBlockMutationQueue(recipient) 37 + 38 + const leaveConvoControl = useDialogControl() 39 + const reportControl = useDialogControl() 40 + const blockedByListControl = useDialogControl() 41 + 42 + const {listBlocks, userBlock} = blockInfo 43 + const isBlocking = !!userBlock || !!listBlocks.length 44 + 45 + const onUnblockPress = React.useCallback(() => { 46 + if (listBlocks.length) { 47 + blockedByListControl.open() 48 + } else { 49 + queueUnblock() 50 + } 51 + }, [blockedByListControl, listBlocks, queueUnblock]) 52 + 53 + return ( 54 + <View style={[hasMessages && a.pt_md, a.pb_xl, a.gap_lg]}> 55 + <Divider /> 56 + <Text style={[a.text_md, a.font_bold, a.text_center]}> 57 + {isBlocking ? ( 58 + <Trans>You have blocked this user</Trans> 59 + ) : ( 60 + <Trans>This user has blocked you</Trans> 61 + )} 62 + </Text> 63 + 64 + <View style={[a.flex_row, a.justify_between, a.gap_lg, a.px_md]}> 65 + <Button 66 + label={_(msg`Leave chat`)} 67 + color="secondary" 68 + variant="solid" 69 + size="small" 70 + style={[a.flex_1]} 71 + onPress={leaveConvoControl.open}> 72 + <ButtonText style={{color: t.palette.negative_500}}> 73 + <Trans>Leave chat</Trans> 74 + </ButtonText> 75 + </Button> 76 + <Button 77 + label={_(msg`Report`)} 78 + color="secondary" 79 + variant="solid" 80 + size="small" 81 + style={[a.flex_1]} 82 + onPress={reportControl.open}> 83 + <ButtonText style={{color: t.palette.negative_500}}> 84 + <Trans>Report</Trans> 85 + </ButtonText> 86 + </Button> 87 + {isBlocking && gtMobile && ( 88 + <Button 89 + label={_(msg`Unblock`)} 90 + color="secondary" 91 + variant="solid" 92 + size="small" 93 + style={[a.flex_1]} 94 + onPress={onUnblockPress}> 95 + <ButtonText style={{color: t.palette.primary_500}}> 96 + <Trans>Unblock</Trans> 97 + </ButtonText> 98 + </Button> 99 + )} 100 + </View> 101 + {isBlocking && !gtMobile && ( 102 + <View style={[a.flex_row, a.justify_center, a.px_md]}> 103 + <Button 104 + label={_(msg`Unblock`)} 105 + color="secondary" 106 + variant="solid" 107 + size="small" 108 + style={[a.flex_1]} 109 + onPress={onUnblockPress}> 110 + <ButtonText style={{color: t.palette.primary_500}}> 111 + <Trans>Unblock</Trans> 112 + </ButtonText> 113 + </Button> 114 + </View> 115 + )} 116 + 117 + <LeaveConvoPrompt 118 + control={leaveConvoControl} 119 + currentScreen="conversation" 120 + convoId={convoId} 121 + /> 122 + 123 + <ReportConversationPrompt control={reportControl} /> 124 + 125 + <BlockedByListDialog 126 + control={blockedByListControl} 127 + listBlocks={listBlocks} 128 + /> 129 + </View> 130 + ) 131 + }
+194
src/components/dms/MessagesListHeader.tsx
··· 1 + import React, {useCallback} from 'react' 2 + import {TouchableOpacity, View} from 'react-native' 3 + import { 4 + AppBskyActorDefs, 5 + ModerationCause, 6 + ModerationDecision, 7 + } from '@atproto/api' 8 + import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 9 + import {msg} from '@lingui/macro' 10 + import {useLingui} from '@lingui/react' 11 + import {useNavigation} from '@react-navigation/native' 12 + 13 + import {BACK_HITSLOP} from 'lib/constants' 14 + import {makeProfileLink} from 'lib/routes/links' 15 + import {NavigationProp} from 'lib/routes/types' 16 + import {sanitizeDisplayName} from 'lib/strings/display-names' 17 + import {isWeb} from 'platform/detection' 18 + import {useProfileShadow} from 'state/cache/profile-shadow' 19 + import {isConvoActive, useConvo} from 'state/messages/convo' 20 + import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' 21 + import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 22 + import {ConvoMenu} from '#/components/dms/ConvoMenu' 23 + import {Link} from '#/components/Link' 24 + import {Text} from '#/components/Typography' 25 + 26 + const PFP_SIZE = isWeb ? 40 : 34 27 + 28 + export let MessagesListHeader = ({ 29 + profile, 30 + moderation, 31 + blockInfo, 32 + }: { 33 + profile?: AppBskyActorDefs.ProfileViewBasic 34 + moderation?: ModerationDecision 35 + blockInfo?: { 36 + listBlocks: ModerationCause[] 37 + userBlock?: ModerationCause 38 + } 39 + }): React.ReactNode => { 40 + const t = useTheme() 41 + const {_} = useLingui() 42 + const {gtTablet} = useBreakpoints() 43 + const navigation = useNavigation<NavigationProp>() 44 + 45 + const onPressBack = useCallback(() => { 46 + if (isWeb) { 47 + navigation.replace('Messages', {}) 48 + } else { 49 + navigation.goBack() 50 + } 51 + }, [navigation]) 52 + 53 + return ( 54 + <View 55 + style={[ 56 + t.atoms.bg, 57 + t.atoms.border_contrast_low, 58 + a.border_b, 59 + a.flex_row, 60 + a.align_center, 61 + a.gap_sm, 62 + gtTablet ? a.pl_lg : a.pl_xl, 63 + a.pr_lg, 64 + a.py_sm, 65 + ]}> 66 + {!gtTablet && ( 67 + <TouchableOpacity 68 + testID="conversationHeaderBackBtn" 69 + onPress={onPressBack} 70 + hitSlop={BACK_HITSLOP} 71 + style={{width: 30, height: 30}} 72 + accessibilityRole="button" 73 + accessibilityLabel={_(msg`Back`)} 74 + accessibilityHint=""> 75 + <FontAwesomeIcon 76 + size={18} 77 + icon="angle-left" 78 + style={{ 79 + marginTop: 6, 80 + }} 81 + color={t.atoms.text.color} 82 + /> 83 + </TouchableOpacity> 84 + )} 85 + 86 + {profile && moderation && blockInfo ? ( 87 + <HeaderReady 88 + profile={profile} 89 + moderation={moderation} 90 + blockInfo={blockInfo} 91 + /> 92 + ) : ( 93 + <> 94 + <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}> 95 + <View 96 + style={[ 97 + {width: PFP_SIZE, height: PFP_SIZE}, 98 + a.rounded_full, 99 + t.atoms.bg_contrast_25, 100 + ]} 101 + /> 102 + <View style={a.gap_xs}> 103 + <View 104 + style={[ 105 + {width: 120, height: 16}, 106 + a.rounded_xs, 107 + t.atoms.bg_contrast_25, 108 + a.mt_xs, 109 + ]} 110 + /> 111 + <View 112 + style={[ 113 + {width: 175, height: 12}, 114 + a.rounded_xs, 115 + t.atoms.bg_contrast_25, 116 + ]} 117 + /> 118 + </View> 119 + </View> 120 + 121 + <View style={{width: 30}} /> 122 + </> 123 + )} 124 + </View> 125 + ) 126 + } 127 + MessagesListHeader = React.memo(MessagesListHeader) 128 + 129 + function HeaderReady({ 130 + profile: profileUnshadowed, 131 + moderation, 132 + blockInfo, 133 + }: { 134 + profile: AppBskyActorDefs.ProfileViewBasic 135 + moderation: ModerationDecision 136 + blockInfo: { 137 + listBlocks: ModerationCause[] 138 + userBlock?: ModerationCause 139 + } 140 + }) { 141 + const t = useTheme() 142 + const convoState = useConvo() 143 + const profile = useProfileShadow(profileUnshadowed) 144 + 145 + const isDeletedAccount = profile?.handle === 'missing.invalid' 146 + const displayName = isDeletedAccount 147 + ? 'Deleted Account' 148 + : sanitizeDisplayName( 149 + profile.displayName || profile.handle, 150 + moderation.ui('displayName'), 151 + ) 152 + 153 + return ( 154 + <> 155 + <Link 156 + style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]} 157 + to={makeProfileLink(profile)}> 158 + <PreviewableUserAvatar 159 + size={PFP_SIZE} 160 + profile={profile} 161 + moderation={moderation.ui('avatar')} 162 + disableHoverCard={moderation.blocked} 163 + /> 164 + <View style={a.flex_1}> 165 + <Text 166 + style={[a.text_md, a.font_bold, web(a.leading_normal)]} 167 + numberOfLines={1}> 168 + {displayName} 169 + </Text> 170 + {!isDeletedAccount && ( 171 + <Text 172 + style={[ 173 + t.atoms.text_contrast_medium, 174 + a.text_sm, 175 + web([a.leading_normal, {marginTop: -2}]), 176 + ]} 177 + numberOfLines={1}> 178 + @{profile.handle} 179 + </Text> 180 + )} 181 + </View> 182 + </Link> 183 + 184 + {isConvoActive(convoState) && ( 185 + <ConvoMenu 186 + convo={convoState.convo} 187 + profile={profile} 188 + currentScreen="conversation" 189 + blockInfo={blockInfo} 190 + /> 191 + )} 192 + </> 193 + ) 194 + }
+27
src/components/dms/ReportConversationPrompt.tsx
··· 1 + import React from 'react' 2 + import {msg} from '@lingui/macro' 3 + import {useLingui} from '@lingui/react' 4 + 5 + import {DialogControlProps} from '#/components/Dialog' 6 + import * as Prompt from '#/components/Prompt' 7 + 8 + export function ReportConversationPrompt({ 9 + control, 10 + }: { 11 + control: DialogControlProps 12 + }) { 13 + const {_} = useLingui() 14 + 15 + return ( 16 + <Prompt.Basic 17 + control={control} 18 + title={_(msg`Report conversation`)} 19 + description={_( 20 + 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.`, 21 + )} 22 + confirmButtonCta={_(msg`I understand`)} 23 + onConfirm={() => {}} 24 + showCancel={false} 25 + /> 26 + ) 27 + }
+3 -3
src/lib/routes/types.ts
··· 72 72 } 73 73 74 74 export type MessagesTabNavigatorParams = CommonNavigatorParams & { 75 - Messages: {pushToConversation?: string} 75 + Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} 76 76 } 77 77 78 78 export type FlatNavigatorParams = CommonNavigatorParams & { ··· 81 81 Feeds: undefined 82 82 Notifications: undefined 83 83 Hashtag: {tag: string; author?: string} 84 - Messages: {pushToConversation?: string} 84 + Messages: {pushToConversation?: string; animation?: 'push' | 'pop'} 85 85 } 86 86 87 87 export type AllNavigatorParams = CommonNavigatorParams & { ··· 96 96 MyProfileTab: undefined 97 97 Hashtag: {tag: string; author?: string} 98 98 MessagesTab: undefined 99 - Messages: undefined 99 + Messages: {animation?: 'push' | 'pop'} 100 100 } 101 101 102 102 // NOTE
+28 -22
src/screens/Messages/Conversation/MessagesList.tsx
··· 23 23 import {List} from 'view/com/util/List' 24 24 import {MessageInput} from '#/screens/Messages/Conversation/MessageInput' 25 25 import {MessageListError} from '#/screens/Messages/Conversation/MessageListError' 26 - import {atoms as a, useBreakpoints} from '#/alf' 26 + import {atoms as a} from '#/alf' 27 27 import {MessageItem} from '#/components/dms/MessageItem' 28 28 import {NewMessagesPill} from '#/components/dms/NewMessagesPill' 29 29 import {Loader} from '#/components/Loader' ··· 66 66 export function MessagesList({ 67 67 hasScrolled, 68 68 setHasScrolled, 69 + blocked, 70 + footer, 69 71 }: { 70 72 hasScrolled: boolean 71 73 setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> 74 + blocked?: boolean 75 + footer?: React.ReactNode 72 76 }) { 73 - const convo = useConvoActive() 77 + const convoState = useConvoActive() 74 78 const {getAgent} = useAgent() 79 + 75 80 const flatListRef = useAnimatedRef<FlatList>() 76 81 77 82 const [showNewMessagesPill, setShowNewMessagesPill] = React.useState(false) ··· 81 86 // the bottom. 82 87 const isAtBottom = useSharedValue(true) 83 88 84 - // This will be used on web to assist in determing if we need to maintain the content offset 89 + // This will be used on web to assist in determining if we need to maintain the content offset 85 90 const isAtTop = useSharedValue(true) 86 91 87 92 // Used to keep track of the current content height. We'll need this in `onScroll` so we know when to start allowing ··· 126 131 if ( 127 132 hasScrolled && 128 133 height - contentHeight.value > layoutHeight.value - 50 && 129 - convo.items.length - prevItemCount.current > 1 134 + convoState.items.length - prevItemCount.current > 1 130 135 ) { 131 136 newOffset = contentHeight.value - 50 132 137 setShowNewMessagesPill(true) 133 - } else if (!hasScrolled && !convo.isFetchingHistory) { 138 + } else if (!hasScrolled && !convoState.isFetchingHistory) { 134 139 setHasScrolled(true) 135 140 } 136 141 ··· 141 146 isMomentumScrolling.value = true 142 147 } 143 148 contentHeight.value = height 144 - prevItemCount.current = convo.items.length 149 + prevItemCount.current = convoState.items.length 145 150 }, 146 151 [ 147 152 hasScrolled, 148 - convo.items.length, 149 - convo.isFetchingHistory, 153 + convoState.items.length, 154 + convoState.isFetchingHistory, 150 155 setHasScrolled, 151 156 // all of these are stable 152 157 contentHeight, ··· 161 166 162 167 const onStartReached = useCallback(() => { 163 168 if (hasScrolled) { 164 - convo.fetchMessageHistory() 169 + convoState.fetchMessageHistory() 165 170 } 166 - }, [convo, hasScrolled]) 171 + }, [convoState, hasScrolled]) 167 172 168 173 const onSendMessage = useCallback( 169 174 async (text: string) => { ··· 182 187 return true 183 188 }) 184 189 185 - convo.sendMessage({ 190 + convoState.sendMessage({ 186 191 text: rt.text, 187 192 facets: rt.facets, 188 193 }) 189 194 }, 190 - [convo, getAgent], 195 + [convoState, getAgent], 191 196 ) 192 197 193 198 const onScroll = React.useCallback( ··· 225 230 226 231 // -- Keyboard animation handling 227 232 const animatedKeyboard = useAnimatedKeyboard() 228 - const {gtMobile} = useBreakpoints() 229 233 const {bottom: bottomInset} = useSafeAreaInsets() 230 234 const nativeBottomBarHeight = isIOS ? 42 : 60 231 - const bottomOffset = 232 - isWeb && gtMobile ? 0 : bottomInset + nativeBottomBarHeight 235 + const bottomOffset = isWeb ? 0 : bottomInset + nativeBottomBarHeight 233 236 234 237 // On web, we don't want to do anything. 235 238 // On native, we want to scroll the list to the bottom every frame that the keyboard is opening. `scrollTo` runs ··· 268 271 <ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}> 269 272 <List 270 273 ref={flatListRef} 271 - data={convo.items} 274 + data={convoState.items} 272 275 renderItem={renderItem} 273 276 keyExtractor={keyExtractor} 274 277 containWeb={true} 275 - contentContainerStyle={[a.px_md]} 276 278 disableVirtualization={true} 277 279 // The extra two items account for the header and the footer components 278 280 initialNumToRender={isNative ? 32 : 62} ··· 289 291 onScrollToIndexFailed={onScrollToIndexFailed} 290 292 scrollEventThrottle={100} 291 293 ListHeaderComponent={ 292 - <MaybeLoader isLoading={convo.isFetchingHistory} /> 294 + <MaybeLoader isLoading={convoState.isFetchingHistory} /> 293 295 } 294 296 /> 295 297 </ScrollProvider> 296 - <MessageInput 297 - onSendMessage={onSendMessage} 298 - scrollToEnd={scrollToEndNow} 299 - /> 298 + {!blocked ? ( 299 + <MessageInput 300 + onSendMessage={onSendMessage} 301 + scrollToEnd={scrollToEndNow} 302 + /> 303 + ) : ( 304 + footer 305 + )} 300 306 {showNewMessagesPill && <NewMessagesPill />} 301 307 </Animated.View> 302 308 )
+60 -166
src/screens/Messages/Conversation/index.tsx
··· 1 1 import React, {useCallback} from 'react' 2 - import {TouchableOpacity, View} from 'react-native' 2 + import {View} from 'react-native' 3 3 import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api' 4 - import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' 5 4 import {msg} from '@lingui/macro' 6 5 import {useLingui} from '@lingui/react' 7 - import {useFocusEffect, useNavigation} from '@react-navigation/native' 6 + import {useFocusEffect} from '@react-navigation/native' 8 7 import {NativeStackScreenProps} from '@react-navigation/native-stack' 9 8 10 - import {makeProfileLink} from '#/lib/routes/links' 11 - import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' 9 + import {CommonNavigatorParams} from '#/lib/routes/types' 12 10 import {useGate} from '#/lib/statsig/statsig' 13 - import {useProfileShadow} from '#/state/cache/profile-shadow' 14 11 import {useCurrentConvoId} from '#/state/messages/current-convo-id' 15 12 import {useModerationOpts} from '#/state/preferences/moderation-opts' 16 13 import {useProfileQuery} from '#/state/queries/profile' 17 - import {BACK_HITSLOP} from 'lib/constants' 18 - import {sanitizeDisplayName} from 'lib/strings/display-names' 19 14 import {isWeb} from 'platform/detection' 15 + import {useProfileShadow} from 'state/cache/profile-shadow' 20 16 import {ConvoProvider, isConvoActive, useConvo} from 'state/messages/convo' 21 17 import {ConvoStatus} from 'state/messages/convo/types' 22 18 import {useSetMinimalShellMode} from 'state/shell' 23 - import {PreviewableUserAvatar} from 'view/com/util/UserAvatar' 24 19 import {CenteredView} from 'view/com/util/Views' 25 20 import {MessagesList} from '#/screens/Messages/Conversation/MessagesList' 26 - import {atoms as a, useBreakpoints, useTheme, web} from '#/alf' 27 - import {ConvoMenu} from '#/components/dms/ConvoMenu' 21 + import {atoms as a, useBreakpoints, useTheme} from '#/alf' 22 + import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter' 23 + import {MessagesListHeader} from '#/components/dms/MessagesListHeader' 28 24 import {Error} from '#/components/Error' 29 - import {Link} from '#/components/Link' 30 - import {ListMaybePlaceholder} from '#/components/Lists' 31 25 import {Loader} from '#/components/Loader' 32 - import {Text} from '#/components/Typography' 33 26 import {ClipClopGate} from '../gate' 34 27 35 28 type Props = NativeStackScreenProps< ··· 73 66 const convoState = useConvo() 74 67 const {_} = useLingui() 75 68 69 + const moderationOpts = useModerationOpts() 70 + const {data: recipient} = useProfileQuery({ 71 + did: convoState.recipients?.[0].did, 72 + }) 73 + 76 74 // Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user, 77 75 // we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be 78 76 // empty. So, we also check for that possible state as well and render once we can. ··· 86 84 if (convoState.status === ConvoStatus.Error) { 87 85 return ( 88 86 <CenteredView style={a.flex_1} sideBorders> 89 - <Header /> 87 + <MessagesListHeader /> 90 88 <Error 91 89 title={_(msg`Something went wrong`)} 92 90 message={_(msg`We couldn't load this conversation`)} ··· 96 94 ) 97 95 } 98 96 99 - /* 100 - * Any other convo states (atm) are "ready" states 101 - */ 102 97 return ( 103 98 <CenteredView style={[a.flex_1]} sideBorders> 104 - <Header profile={convoState.recipients?.[0]} /> 99 + {!readyToShow && <MessagesListHeader />} 105 100 <View style={[a.flex_1]}> 106 - {isConvoActive(convoState) ? ( 107 - <MessagesList 101 + {moderationOpts && recipient ? ( 102 + <InnerReady 103 + moderationOpts={moderationOpts} 104 + recipient={recipient} 108 105 hasScrolled={hasScrolled} 109 106 setHasScrolled={setHasScrolled} 110 107 /> 111 108 ) : ( 112 - <ListMaybePlaceholder isLoading /> 109 + <> 110 + <View style={[a.align_center, a.gap_sm, a.flex_1]} /> 111 + </> 113 112 )} 114 113 {!readyToShow && ( 115 114 <View ··· 132 131 ) 133 132 } 134 133 135 - const PFP_SIZE = isWeb ? 40 : 34 136 - 137 - let Header = ({ 138 - profile: initialProfile, 139 - }: { 140 - profile?: AppBskyActorDefs.ProfileViewBasic 141 - }): React.ReactNode => { 142 - const t = useTheme() 143 - const {_} = useLingui() 144 - const {gtTablet} = useBreakpoints() 145 - const navigation = useNavigation<NavigationProp>() 146 - const moderationOpts = useModerationOpts() 147 - const {data: profile} = useProfileQuery({did: initialProfile?.did}) 148 - 149 - const onPressBack = useCallback(() => { 150 - if (isWeb) { 151 - navigation.replace('Messages') 152 - } else { 153 - navigation.goBack() 154 - } 155 - }, [navigation]) 156 - 157 - return ( 158 - <View 159 - style={[ 160 - t.atoms.bg, 161 - t.atoms.border_contrast_low, 162 - a.border_b, 163 - a.flex_row, 164 - a.align_center, 165 - a.gap_sm, 166 - gtTablet ? a.pl_lg : a.pl_xl, 167 - a.pr_lg, 168 - a.py_sm, 169 - ]}> 170 - {!gtTablet && ( 171 - <TouchableOpacity 172 - testID="conversationHeaderBackBtn" 173 - onPress={onPressBack} 174 - hitSlop={BACK_HITSLOP} 175 - style={{width: 30, height: 30}} 176 - accessibilityRole="button" 177 - accessibilityLabel={_(msg`Back`)} 178 - accessibilityHint=""> 179 - <FontAwesomeIcon 180 - size={18} 181 - icon="angle-left" 182 - style={{ 183 - marginTop: 6, 184 - }} 185 - color={t.atoms.text.color} 186 - /> 187 - </TouchableOpacity> 188 - )} 189 - 190 - {profile && moderationOpts ? ( 191 - <HeaderReady profile={profile} moderationOpts={moderationOpts} /> 192 - ) : ( 193 - <> 194 - <View style={[a.flex_row, a.align_center, a.gap_md, a.flex_1]}> 195 - <View 196 - style={[ 197 - {width: PFP_SIZE, height: PFP_SIZE}, 198 - a.rounded_full, 199 - t.atoms.bg_contrast_25, 200 - ]} 201 - /> 202 - <View style={a.gap_xs}> 203 - <View 204 - style={[ 205 - {width: 120, height: 16}, 206 - a.rounded_xs, 207 - t.atoms.bg_contrast_25, 208 - a.mt_xs, 209 - ]} 210 - /> 211 - <View 212 - style={[ 213 - {width: 175, height: 12}, 214 - a.rounded_xs, 215 - t.atoms.bg_contrast_25, 216 - ]} 217 - /> 218 - </View> 219 - </View> 220 - 221 - <View style={{width: 30}} /> 222 - </> 223 - )} 224 - </View> 225 - ) 226 - } 227 - Header = React.memo(Header) 228 - 229 - function HeaderReady({ 230 - profile: profileUnshadowed, 134 + function InnerReady({ 231 135 moderationOpts, 136 + recipient: recipientUnshadowed, 137 + hasScrolled, 138 + setHasScrolled, 232 139 }: { 233 - profile: AppBskyActorDefs.ProfileViewBasic 234 140 moderationOpts: ModerationOpts 141 + recipient: AppBskyActorDefs.ProfileViewBasic 142 + hasScrolled: boolean 143 + setHasScrolled: React.Dispatch<React.SetStateAction<boolean>> 235 144 }) { 236 - const t = useTheme() 237 145 const convoState = useConvo() 238 - const profile = useProfileShadow(profileUnshadowed) 239 - const moderation = React.useMemo( 240 - () => moderateProfile(profile, moderationOpts), 241 - [profile, moderationOpts], 242 - ) 146 + const recipient = useProfileShadow(recipientUnshadowed) 243 147 244 - const isDeletedAccount = profile?.handle === 'missing.invalid' 245 - const displayName = isDeletedAccount 246 - ? 'Deleted Account' 247 - : sanitizeDisplayName( 248 - profile.displayName || profile.handle, 249 - moderation.ui('displayName'), 250 - ) 148 + const moderation = React.useMemo(() => { 149 + return moderateProfile(recipient, moderationOpts) 150 + }, [recipient, moderationOpts]) 151 + 152 + const blockInfo = React.useMemo(() => { 153 + const modui = moderation.ui('profileView') 154 + const blocks = modui.alerts.filter(alert => alert.type === 'blocking') 155 + const listBlocks = blocks.filter(alert => alert.source.type === 'list') 156 + const userBlock = blocks.find(alert => alert.source.type === 'user') 157 + return { 158 + listBlocks, 159 + userBlock, 160 + } 161 + }, [moderation]) 251 162 252 163 return ( 253 164 <> 254 - <Link 255 - style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]} 256 - to={makeProfileLink(profile)}> 257 - <PreviewableUserAvatar 258 - size={PFP_SIZE} 259 - profile={profile} 260 - moderation={moderation.ui('avatar')} 261 - disableHoverCard={moderation.blocked} 262 - /> 263 - <View style={a.flex_1}> 264 - <Text 265 - style={[a.text_md, a.font_bold, web(a.leading_normal)]} 266 - numberOfLines={1}> 267 - {displayName} 268 - </Text> 269 - {!isDeletedAccount && ( 270 - <Text 271 - style={[ 272 - t.atoms.text_contrast_medium, 273 - a.text_sm, 274 - web([a.leading_normal, {marginTop: -2}]), 275 - ]} 276 - numberOfLines={1}> 277 - @{profile.handle} 278 - </Text> 279 - )} 280 - </View> 281 - </Link> 282 - 165 + <MessagesListHeader 166 + profile={recipient} 167 + moderation={moderation} 168 + blockInfo={blockInfo} 169 + /> 283 170 {isConvoActive(convoState) && ( 284 - <ConvoMenu 285 - convo={convoState.convo} 286 - profile={profile} 287 - currentScreen="conversation" 288 - moderation={moderation} 171 + <MessagesList 172 + hasScrolled={hasScrolled} 173 + setHasScrolled={setHasScrolled} 174 + blocked={moderation?.blocked} 175 + footer={ 176 + <MessagesListBlockedFooter 177 + recipient={recipient} 178 + convoId={convoState.convo.id} 179 + hasMessages={convoState.items.length > 0} 180 + blockInfo={blockInfo} 181 + /> 182 + } 289 183 /> 290 184 )} 291 185 </>
+12 -1
src/screens/Messages/List/ChatListItem.tsx
··· 65 65 [profile, moderationOpts], 66 66 ) 67 67 68 + const blockInfo = React.useMemo(() => { 69 + const modui = moderation.ui('profileView') 70 + const blocks = modui.alerts.filter(alert => alert.type === 'blocking') 71 + const listBlocks = blocks.filter(alert => alert.source.type === 'list') 72 + const userBlock = blocks.find(alert => alert.source.type === 'user') 73 + return { 74 + listBlocks, 75 + userBlock, 76 + } 77 + }, [moderation]) 78 + 68 79 const isDeletedAccount = profile.handle === 'missing.invalid' 69 80 const displayName = isDeletedAccount 70 81 ? 'Deleted Account' ··· 241 252 triggerOpacity={ 242 253 !gtMobile || showActions || menuControl.isOpen ? 1 : 0 243 254 } 244 - moderation={moderation} 255 + blockInfo={blockInfo} 245 256 /> 246 257 </View> 247 258 </View>