[🐴] 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
zio/stable
parent
1b47ea7367
commit
d02e0884c4
|
@ -464,7 +464,10 @@ function MessagesTabNavigator() {
|
||||||
<MessagesTab.Screen
|
<MessagesTab.Screen
|
||||||
name="Messages"
|
name="Messages"
|
||||||
getComponent={() => MessagesScreen}
|
getComponent={() => MessagesScreen}
|
||||||
options={{requireAuth: true}}
|
options={({route}) => ({
|
||||||
|
requireAuth: true,
|
||||||
|
animationTypeForReplace: route.params?.animation ?? 'push',
|
||||||
|
})}
|
||||||
/>
|
/>
|
||||||
{commonScreens(MessagesTab as typeof HomeTab)}
|
{commonScreens(MessagesTab as typeof HomeTab)}
|
||||||
</MessagesTab.Navigator>
|
</MessagesTab.Navigator>
|
||||||
|
|
|
@ -172,6 +172,7 @@ export function Basic({
|
||||||
confirmButtonCta,
|
confirmButtonCta,
|
||||||
onConfirm,
|
onConfirm,
|
||||||
confirmButtonColor,
|
confirmButtonColor,
|
||||||
|
showCancel = true,
|
||||||
}: React.PropsWithChildren<{
|
}: React.PropsWithChildren<{
|
||||||
control: Dialog.DialogOuterProps['control']
|
control: Dialog.DialogOuterProps['control']
|
||||||
title: string
|
title: string
|
||||||
|
@ -187,6 +188,7 @@ export function Basic({
|
||||||
*/
|
*/
|
||||||
onConfirm: () => void
|
onConfirm: () => void
|
||||||
confirmButtonColor?: ButtonColor
|
confirmButtonColor?: ButtonColor
|
||||||
|
showCancel?: boolean
|
||||||
}>) {
|
}>) {
|
||||||
return (
|
return (
|
||||||
<Outer control={control} testID="confirmModal">
|
<Outer control={control} testID="confirmModal">
|
||||||
|
@ -199,7 +201,7 @@ export function Basic({
|
||||||
color={confirmButtonColor}
|
color={confirmButtonColor}
|
||||||
testID="confirmBtn"
|
testID="confirmBtn"
|
||||||
/>
|
/>
|
||||||
<Cancel cta={cancelButtonCta} />
|
{showCancel && <Cancel cta={cancelButtonCta} />}
|
||||||
</Actions>
|
</Actions>
|
||||||
</Outer>
|
</Outer>
|
||||||
)
|
)
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -3,25 +3,25 @@ import {Keyboard, Pressable, View} from 'react-native'
|
||||||
import {
|
import {
|
||||||
AppBskyActorDefs,
|
AppBskyActorDefs,
|
||||||
ChatBskyConvoDefs,
|
ChatBskyConvoDefs,
|
||||||
ModerationDecision,
|
ModerationCause,
|
||||||
} from '@atproto/api'
|
} from '@atproto/api'
|
||||||
import {msg, Trans} from '@lingui/macro'
|
import {msg, Trans} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useNavigation} from '@react-navigation/native'
|
import {useNavigation} from '@react-navigation/native'
|
||||||
|
|
||||||
import {NavigationProp} from '#/lib/routes/types'
|
import {NavigationProp} from '#/lib/routes/types'
|
||||||
import {listUriToHref} from '#/lib/strings/url-helpers'
|
|
||||||
import {Shadow} from '#/state/cache/types'
|
import {Shadow} from '#/state/cache/types'
|
||||||
import {
|
import {
|
||||||
useConvoQuery,
|
useConvoQuery,
|
||||||
useMarkAsReadMutation,
|
useMarkAsReadMutation,
|
||||||
} from '#/state/queries/messages/conversation'
|
} from '#/state/queries/messages/conversation'
|
||||||
import {useLeaveConvo} from '#/state/queries/messages/leave-conversation'
|
|
||||||
import {useMuteConvo} from '#/state/queries/messages/mute-conversation'
|
import {useMuteConvo} from '#/state/queries/messages/mute-conversation'
|
||||||
import {useProfileBlockMutationQueue} from '#/state/queries/profile'
|
import {useProfileBlockMutationQueue} from '#/state/queries/profile'
|
||||||
import * as Toast from '#/view/com/util/Toast'
|
import * as Toast from '#/view/com/util/Toast'
|
||||||
import {atoms as a, useTheme} from '#/alf'
|
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 {ArrowBoxLeft_Stroke2_Corner0_Rounded as ArrowBoxLeft} from '#/components/icons/ArrowBoxLeft'
|
||||||
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
import {DotGrid_Stroke2_Corner0_Rounded as DotsHorizontal} from '#/components/icons/DotGrid'
|
||||||
import {Flag_Stroke2_Corner0_Rounded as Flag} from '#/components/icons/Flag'
|
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 {PersonCheck_Stroke2_Corner0_Rounded as PersonCheck} from '#/components/icons/PersonCheck'
|
||||||
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
|
import {PersonX_Stroke2_Corner0_Rounded as PersonX} from '#/components/icons/PersonX'
|
||||||
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
import {SpeakerVolumeFull_Stroke2_Corner0_Rounded as Unmute} from '#/components/icons/Speaker'
|
||||||
import {InlineLinkText} from '#/components/Link'
|
|
||||||
import * as Menu from '#/components/Menu'
|
import * as Menu from '#/components/Menu'
|
||||||
import * as Prompt from '#/components/Prompt'
|
import * as Prompt from '#/components/Prompt'
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble'
|
import {Bubble_Stroke2_Corner2_Rounded as Bubble} from '../icons/Bubble'
|
||||||
|
|
||||||
let ConvoMenu = ({
|
let ConvoMenu = ({
|
||||||
|
@ -44,7 +42,7 @@ let ConvoMenu = ({
|
||||||
showMarkAsRead,
|
showMarkAsRead,
|
||||||
hideTrigger,
|
hideTrigger,
|
||||||
triggerOpacity,
|
triggerOpacity,
|
||||||
moderation,
|
blockInfo,
|
||||||
}: {
|
}: {
|
||||||
convo: ChatBskyConvoDefs.ConvoView
|
convo: ChatBskyConvoDefs.ConvoView
|
||||||
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
profile: Shadow<AppBskyActorDefs.ProfileViewBasic>
|
||||||
|
@ -53,7 +51,10 @@ let ConvoMenu = ({
|
||||||
showMarkAsRead?: boolean
|
showMarkAsRead?: boolean
|
||||||
hideTrigger?: boolean
|
hideTrigger?: boolean
|
||||||
triggerOpacity?: number
|
triggerOpacity?: number
|
||||||
moderation: ModerationDecision
|
blockInfo: {
|
||||||
|
listBlocks: ModerationCause[]
|
||||||
|
userBlock?: ModerationCause
|
||||||
|
}
|
||||||
}): React.ReactNode => {
|
}): React.ReactNode => {
|
||||||
const navigation = useNavigation<NavigationProp>()
|
const navigation = useNavigation<NavigationProp>()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
@ -62,17 +63,9 @@ let ConvoMenu = ({
|
||||||
const reportControl = Prompt.usePromptControl()
|
const reportControl = Prompt.usePromptControl()
|
||||||
const blockedByListControl = Prompt.usePromptControl()
|
const blockedByListControl = Prompt.usePromptControl()
|
||||||
const {mutate: markAsRead} = useMarkAsReadMutation()
|
const {mutate: markAsRead} = useMarkAsReadMutation()
|
||||||
const modui = moderation.ui('profileView')
|
|
||||||
const {listBlocks, userBlock} = React.useMemo(() => {
|
const {listBlocks, userBlock} = blockInfo
|
||||||
const blocks = modui.alerts.filter(alert => alert.type === 'blocking')
|
const isBlocking = userBlock || !!listBlocks.length
|
||||||
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)
|
const {data: convo} = useConvoQuery(initialConvo)
|
||||||
|
|
||||||
|
@ -108,17 +101,6 @@ let ConvoMenu = ({
|
||||||
}
|
}
|
||||||
}, [userBlock, listBlocks, blockedByListControl, queueBlock, queueUnblock])
|
}, [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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<Menu.Root control={control}>
|
<Menu.Root control={control}>
|
||||||
|
@ -218,67 +200,19 @@ let ConvoMenu = ({
|
||||||
</Menu.Outer>
|
</Menu.Outer>
|
||||||
</Menu.Root>
|
</Menu.Root>
|
||||||
|
|
||||||
<Prompt.Basic
|
<LeaveConvoPrompt
|
||||||
control={leaveConvoControl}
|
control={leaveConvoControl}
|
||||||
title={_(msg`Leave conversation`)}
|
convoId={convo.id}
|
||||||
description={_(
|
currentScreen={currentScreen}
|
||||||
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()}
|
|
||||||
/>
|
/>
|
||||||
|
<ReportConversationPrompt control={reportControl} />
|
||||||
<Prompt.Basic
|
<BlockedByListDialog
|
||||||
control={reportControl}
|
control={blockedByListControl}
|
||||||
title={_(msg`Report conversation`)}
|
listBlocks={listBlocks}
|
||||||
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}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<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)
|
ConvoMenu = React.memo(ConvoMenu)
|
||||||
|
|
||||||
export {ConvoMenu}
|
export {ConvoMenu}
|
||||||
|
|
||||||
function noop() {}
|
|
||||||
|
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -75,7 +75,7 @@ let MessageItem = ({
|
||||||
}, [message.text, message.facets])
|
}, [message.text, message.facets])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View>
|
<View style={[isFromSelf ? a.mr_md : a.ml_md]}>
|
||||||
<ActionsWrapper isFromSelf={isFromSelf} message={message}>
|
<ActionsWrapper isFromSelf={isFromSelf} message={message}>
|
||||||
<View
|
<View
|
||||||
style={[
|
style={[
|
||||||
|
|
|
@ -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>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)
|
||||||
|
}
|
|
@ -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}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
|
@ -72,7 +72,7 @@ export type MyProfileTabNavigatorParams = CommonNavigatorParams & {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MessagesTabNavigatorParams = CommonNavigatorParams & {
|
export type MessagesTabNavigatorParams = CommonNavigatorParams & {
|
||||||
Messages: {pushToConversation?: string}
|
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type FlatNavigatorParams = CommonNavigatorParams & {
|
export type FlatNavigatorParams = CommonNavigatorParams & {
|
||||||
|
@ -81,7 +81,7 @@ export type FlatNavigatorParams = CommonNavigatorParams & {
|
||||||
Feeds: undefined
|
Feeds: undefined
|
||||||
Notifications: undefined
|
Notifications: undefined
|
||||||
Hashtag: {tag: string; author?: string}
|
Hashtag: {tag: string; author?: string}
|
||||||
Messages: {pushToConversation?: string}
|
Messages: {pushToConversation?: string; animation?: 'push' | 'pop'}
|
||||||
}
|
}
|
||||||
|
|
||||||
export type AllNavigatorParams = CommonNavigatorParams & {
|
export type AllNavigatorParams = CommonNavigatorParams & {
|
||||||
|
@ -96,7 +96,7 @@ export type AllNavigatorParams = CommonNavigatorParams & {
|
||||||
MyProfileTab: undefined
|
MyProfileTab: undefined
|
||||||
Hashtag: {tag: string; author?: string}
|
Hashtag: {tag: string; author?: string}
|
||||||
MessagesTab: undefined
|
MessagesTab: undefined
|
||||||
Messages: undefined
|
Messages: {animation?: 'push' | 'pop'}
|
||||||
}
|
}
|
||||||
|
|
||||||
// NOTE
|
// NOTE
|
||||||
|
|
|
@ -23,7 +23,7 @@ import {isWeb} from 'platform/detection'
|
||||||
import {List} from 'view/com/util/List'
|
import {List} from 'view/com/util/List'
|
||||||
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
|
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
|
||||||
import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
|
import {MessageListError} from '#/screens/Messages/Conversation/MessageListError'
|
||||||
import {atoms as a, useBreakpoints} from '#/alf'
|
import {atoms as a} from '#/alf'
|
||||||
import {MessageItem} from '#/components/dms/MessageItem'
|
import {MessageItem} from '#/components/dms/MessageItem'
|
||||||
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
import {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
|
@ -66,12 +66,17 @@ function onScrollToIndexFailed() {
|
||||||
export function MessagesList({
|
export function MessagesList({
|
||||||
hasScrolled,
|
hasScrolled,
|
||||||
setHasScrolled,
|
setHasScrolled,
|
||||||
|
blocked,
|
||||||
|
footer,
|
||||||
}: {
|
}: {
|
||||||
hasScrolled: boolean
|
hasScrolled: boolean
|
||||||
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
|
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
|
blocked?: boolean
|
||||||
|
footer?: React.ReactNode
|
||||||
}) {
|
}) {
|
||||||
const convo = useConvoActive()
|
const convoState = useConvoActive()
|
||||||
const {getAgent} = useAgent()
|
const {getAgent} = useAgent()
|
||||||
|
|
||||||
const flatListRef = useAnimatedRef<FlatList>()
|
const flatListRef = useAnimatedRef<FlatList>()
|
||||||
|
|
||||||
const [showNewMessagesPill, setShowNewMessagesPill] = React.useState(false)
|
const [showNewMessagesPill, setShowNewMessagesPill] = React.useState(false)
|
||||||
|
@ -81,7 +86,7 @@ export function MessagesList({
|
||||||
// the bottom.
|
// the bottom.
|
||||||
const isAtBottom = useSharedValue(true)
|
const isAtBottom = useSharedValue(true)
|
||||||
|
|
||||||
// This will be used on web to assist in determing if we need to maintain the content offset
|
// This will be used on web to assist in determining if we need to maintain the content offset
|
||||||
const isAtTop = useSharedValue(true)
|
const isAtTop = useSharedValue(true)
|
||||||
|
|
||||||
// Used to keep track of the current content height. We'll need this in `onScroll` so we know when to start allowing
|
// Used to keep track of the current content height. We'll need this in `onScroll` so we know when to start allowing
|
||||||
|
@ -126,11 +131,11 @@ export function MessagesList({
|
||||||
if (
|
if (
|
||||||
hasScrolled &&
|
hasScrolled &&
|
||||||
height - contentHeight.value > layoutHeight.value - 50 &&
|
height - contentHeight.value > layoutHeight.value - 50 &&
|
||||||
convo.items.length - prevItemCount.current > 1
|
convoState.items.length - prevItemCount.current > 1
|
||||||
) {
|
) {
|
||||||
newOffset = contentHeight.value - 50
|
newOffset = contentHeight.value - 50
|
||||||
setShowNewMessagesPill(true)
|
setShowNewMessagesPill(true)
|
||||||
} else if (!hasScrolled && !convo.isFetchingHistory) {
|
} else if (!hasScrolled && !convoState.isFetchingHistory) {
|
||||||
setHasScrolled(true)
|
setHasScrolled(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -141,12 +146,12 @@ export function MessagesList({
|
||||||
isMomentumScrolling.value = true
|
isMomentumScrolling.value = true
|
||||||
}
|
}
|
||||||
contentHeight.value = height
|
contentHeight.value = height
|
||||||
prevItemCount.current = convo.items.length
|
prevItemCount.current = convoState.items.length
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
hasScrolled,
|
hasScrolled,
|
||||||
convo.items.length,
|
convoState.items.length,
|
||||||
convo.isFetchingHistory,
|
convoState.isFetchingHistory,
|
||||||
setHasScrolled,
|
setHasScrolled,
|
||||||
// all of these are stable
|
// all of these are stable
|
||||||
contentHeight,
|
contentHeight,
|
||||||
|
@ -161,9 +166,9 @@ export function MessagesList({
|
||||||
|
|
||||||
const onStartReached = useCallback(() => {
|
const onStartReached = useCallback(() => {
|
||||||
if (hasScrolled) {
|
if (hasScrolled) {
|
||||||
convo.fetchMessageHistory()
|
convoState.fetchMessageHistory()
|
||||||
}
|
}
|
||||||
}, [convo, hasScrolled])
|
}, [convoState, hasScrolled])
|
||||||
|
|
||||||
const onSendMessage = useCallback(
|
const onSendMessage = useCallback(
|
||||||
async (text: string) => {
|
async (text: string) => {
|
||||||
|
@ -182,12 +187,12 @@ export function MessagesList({
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
convo.sendMessage({
|
convoState.sendMessage({
|
||||||
text: rt.text,
|
text: rt.text,
|
||||||
facets: rt.facets,
|
facets: rt.facets,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
[convo, getAgent],
|
[convoState, getAgent],
|
||||||
)
|
)
|
||||||
|
|
||||||
const onScroll = React.useCallback(
|
const onScroll = React.useCallback(
|
||||||
|
@ -225,11 +230,9 @@ export function MessagesList({
|
||||||
|
|
||||||
// -- Keyboard animation handling
|
// -- Keyboard animation handling
|
||||||
const animatedKeyboard = useAnimatedKeyboard()
|
const animatedKeyboard = useAnimatedKeyboard()
|
||||||
const {gtMobile} = useBreakpoints()
|
|
||||||
const {bottom: bottomInset} = useSafeAreaInsets()
|
const {bottom: bottomInset} = useSafeAreaInsets()
|
||||||
const nativeBottomBarHeight = isIOS ? 42 : 60
|
const nativeBottomBarHeight = isIOS ? 42 : 60
|
||||||
const bottomOffset =
|
const bottomOffset = isWeb ? 0 : bottomInset + nativeBottomBarHeight
|
||||||
isWeb && gtMobile ? 0 : bottomInset + nativeBottomBarHeight
|
|
||||||
|
|
||||||
// On web, we don't want to do anything.
|
// On web, we don't want to do anything.
|
||||||
// On native, we want to scroll the list to the bottom every frame that the keyboard is opening. `scrollTo` runs
|
// On native, we want to scroll the list to the bottom every frame that the keyboard is opening. `scrollTo` runs
|
||||||
|
@ -268,11 +271,10 @@ export function MessagesList({
|
||||||
<ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}>
|
<ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}>
|
||||||
<List
|
<List
|
||||||
ref={flatListRef}
|
ref={flatListRef}
|
||||||
data={convo.items}
|
data={convoState.items}
|
||||||
renderItem={renderItem}
|
renderItem={renderItem}
|
||||||
keyExtractor={keyExtractor}
|
keyExtractor={keyExtractor}
|
||||||
containWeb={true}
|
containWeb={true}
|
||||||
contentContainerStyle={[a.px_md]}
|
|
||||||
disableVirtualization={true}
|
disableVirtualization={true}
|
||||||
// The extra two items account for the header and the footer components
|
// The extra two items account for the header and the footer components
|
||||||
initialNumToRender={isNative ? 32 : 62}
|
initialNumToRender={isNative ? 32 : 62}
|
||||||
|
@ -289,14 +291,18 @@ export function MessagesList({
|
||||||
onScrollToIndexFailed={onScrollToIndexFailed}
|
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||||
scrollEventThrottle={100}
|
scrollEventThrottle={100}
|
||||||
ListHeaderComponent={
|
ListHeaderComponent={
|
||||||
<MaybeLoader isLoading={convo.isFetchingHistory} />
|
<MaybeLoader isLoading={convoState.isFetchingHistory} />
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</ScrollProvider>
|
</ScrollProvider>
|
||||||
|
{!blocked ? (
|
||||||
<MessageInput
|
<MessageInput
|
||||||
onSendMessage={onSendMessage}
|
onSendMessage={onSendMessage}
|
||||||
scrollToEnd={scrollToEndNow}
|
scrollToEnd={scrollToEndNow}
|
||||||
/>
|
/>
|
||||||
|
) : (
|
||||||
|
footer
|
||||||
|
)}
|
||||||
{showNewMessagesPill && <NewMessagesPill />}
|
{showNewMessagesPill && <NewMessagesPill />}
|
||||||
</Animated.View>
|
</Animated.View>
|
||||||
)
|
)
|
||||||
|
|
|
@ -1,35 +1,28 @@
|
||||||
import React, {useCallback} from 'react'
|
import React, {useCallback} from 'react'
|
||||||
import {TouchableOpacity, View} from 'react-native'
|
import {View} from 'react-native'
|
||||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
|
||||||
import {msg} from '@lingui/macro'
|
import {msg} from '@lingui/macro'
|
||||||
import {useLingui} from '@lingui/react'
|
import {useLingui} from '@lingui/react'
|
||||||
import {useFocusEffect, useNavigation} from '@react-navigation/native'
|
import {useFocusEffect} from '@react-navigation/native'
|
||||||
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
import {NativeStackScreenProps} from '@react-navigation/native-stack'
|
||||||
|
|
||||||
import {makeProfileLink} from '#/lib/routes/links'
|
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
|
||||||
import {useGate} from '#/lib/statsig/statsig'
|
import {useGate} from '#/lib/statsig/statsig'
|
||||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
|
||||||
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
||||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||||
import {useProfileQuery} from '#/state/queries/profile'
|
import {useProfileQuery} from '#/state/queries/profile'
|
||||||
import {BACK_HITSLOP} from 'lib/constants'
|
|
||||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
|
||||||
import {isWeb} from 'platform/detection'
|
import {isWeb} from 'platform/detection'
|
||||||
|
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||||
import {ConvoProvider, isConvoActive, useConvo} from 'state/messages/convo'
|
import {ConvoProvider, isConvoActive, useConvo} from 'state/messages/convo'
|
||||||
import {ConvoStatus} from 'state/messages/convo/types'
|
import {ConvoStatus} from 'state/messages/convo/types'
|
||||||
import {useSetMinimalShellMode} from 'state/shell'
|
import {useSetMinimalShellMode} from 'state/shell'
|
||||||
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
|
|
||||||
import {CenteredView} from 'view/com/util/Views'
|
import {CenteredView} from 'view/com/util/Views'
|
||||||
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
|
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
|
||||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter'
|
||||||
|
import {MessagesListHeader} from '#/components/dms/MessagesListHeader'
|
||||||
import {Error} from '#/components/Error'
|
import {Error} from '#/components/Error'
|
||||||
import {Link} from '#/components/Link'
|
|
||||||
import {ListMaybePlaceholder} from '#/components/Lists'
|
|
||||||
import {Loader} from '#/components/Loader'
|
import {Loader} from '#/components/Loader'
|
||||||
import {Text} from '#/components/Typography'
|
|
||||||
import {ClipClopGate} from '../gate'
|
import {ClipClopGate} from '../gate'
|
||||||
|
|
||||||
type Props = NativeStackScreenProps<
|
type Props = NativeStackScreenProps<
|
||||||
|
@ -73,6 +66,11 @@ function Inner() {
|
||||||
const convoState = useConvo()
|
const convoState = useConvo()
|
||||||
const {_} = useLingui()
|
const {_} = useLingui()
|
||||||
|
|
||||||
|
const moderationOpts = useModerationOpts()
|
||||||
|
const {data: recipient} = useProfileQuery({
|
||||||
|
did: convoState.recipients?.[0].did,
|
||||||
|
})
|
||||||
|
|
||||||
// Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user,
|
// Because we want to give the list a chance to asynchronously scroll to the end before it is visible to the user,
|
||||||
// we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be
|
// we use `hasScrolled` to determine when to render. With that said however, there is a chance that the chat will be
|
||||||
// empty. So, we also check for that possible state as well and render once we can.
|
// empty. So, we also check for that possible state as well and render once we can.
|
||||||
|
@ -86,7 +84,7 @@ function Inner() {
|
||||||
if (convoState.status === ConvoStatus.Error) {
|
if (convoState.status === ConvoStatus.Error) {
|
||||||
return (
|
return (
|
||||||
<CenteredView style={a.flex_1} sideBorders>
|
<CenteredView style={a.flex_1} sideBorders>
|
||||||
<Header />
|
<MessagesListHeader />
|
||||||
<Error
|
<Error
|
||||||
title={_(msg`Something went wrong`)}
|
title={_(msg`Something went wrong`)}
|
||||||
message={_(msg`We couldn't load this conversation`)}
|
message={_(msg`We couldn't load this conversation`)}
|
||||||
|
@ -96,20 +94,21 @@ function Inner() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/*
|
|
||||||
* Any other convo states (atm) are "ready" states
|
|
||||||
*/
|
|
||||||
return (
|
return (
|
||||||
<CenteredView style={[a.flex_1]} sideBorders>
|
<CenteredView style={[a.flex_1]} sideBorders>
|
||||||
<Header profile={convoState.recipients?.[0]} />
|
{!readyToShow && <MessagesListHeader />}
|
||||||
<View style={[a.flex_1]}>
|
<View style={[a.flex_1]}>
|
||||||
{isConvoActive(convoState) ? (
|
{moderationOpts && recipient ? (
|
||||||
<MessagesList
|
<InnerReady
|
||||||
|
moderationOpts={moderationOpts}
|
||||||
|
recipient={recipient}
|
||||||
hasScrolled={hasScrolled}
|
hasScrolled={hasScrolled}
|
||||||
setHasScrolled={setHasScrolled}
|
setHasScrolled={setHasScrolled}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<ListMaybePlaceholder isLoading />
|
<>
|
||||||
|
<View style={[a.align_center, a.gap_sm, a.flex_1]} />
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
{!readyToShow && (
|
{!readyToShow && (
|
||||||
<View
|
<View
|
||||||
|
@ -132,160 +131,55 @@ function Inner() {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const PFP_SIZE = isWeb ? 40 : 34
|
function InnerReady({
|
||||||
|
|
||||||
let Header = ({
|
|
||||||
profile: initialProfile,
|
|
||||||
}: {
|
|
||||||
profile?: AppBskyActorDefs.ProfileViewBasic
|
|
||||||
}): React.ReactNode => {
|
|
||||||
const t = useTheme()
|
|
||||||
const {_} = useLingui()
|
|
||||||
const {gtTablet} = useBreakpoints()
|
|
||||||
const navigation = useNavigation<NavigationProp>()
|
|
||||||
const moderationOpts = useModerationOpts()
|
|
||||||
const {data: profile} = useProfileQuery({did: initialProfile?.did})
|
|
||||||
|
|
||||||
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 && moderationOpts ? (
|
|
||||||
<HeaderReady profile={profile} moderationOpts={moderationOpts} />
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<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>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Header = React.memo(Header)
|
|
||||||
|
|
||||||
function HeaderReady({
|
|
||||||
profile: profileUnshadowed,
|
|
||||||
moderationOpts,
|
moderationOpts,
|
||||||
|
recipient: recipientUnshadowed,
|
||||||
|
hasScrolled,
|
||||||
|
setHasScrolled,
|
||||||
}: {
|
}: {
|
||||||
profile: AppBskyActorDefs.ProfileViewBasic
|
|
||||||
moderationOpts: ModerationOpts
|
moderationOpts: ModerationOpts
|
||||||
|
recipient: AppBskyActorDefs.ProfileViewBasic
|
||||||
|
hasScrolled: boolean
|
||||||
|
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
|
||||||
}) {
|
}) {
|
||||||
const t = useTheme()
|
|
||||||
const convoState = useConvo()
|
const convoState = useConvo()
|
||||||
const profile = useProfileShadow(profileUnshadowed)
|
const recipient = useProfileShadow(recipientUnshadowed)
|
||||||
const moderation = React.useMemo(
|
|
||||||
() => moderateProfile(profile, moderationOpts),
|
|
||||||
[profile, moderationOpts],
|
|
||||||
)
|
|
||||||
|
|
||||||
const isDeletedAccount = profile?.handle === 'missing.invalid'
|
const moderation = React.useMemo(() => {
|
||||||
const displayName = isDeletedAccount
|
return moderateProfile(recipient, moderationOpts)
|
||||||
? 'Deleted Account'
|
}, [recipient, moderationOpts])
|
||||||
: sanitizeDisplayName(
|
|
||||||
profile.displayName || profile.handle,
|
const blockInfo = React.useMemo(() => {
|
||||||
moderation.ui('displayName'),
|
const modui = moderation.ui('profileView')
|
||||||
)
|
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,
|
||||||
|
}
|
||||||
|
}, [moderation])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Link
|
<MessagesListHeader
|
||||||
style={[a.flex_row, a.align_center, a.gap_md, a.flex_1, a.pr_md]}
|
profile={recipient}
|
||||||
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"
|
|
||||||
moderation={moderation}
|
moderation={moderation}
|
||||||
|
blockInfo={blockInfo}
|
||||||
|
/>
|
||||||
|
{isConvoActive(convoState) && (
|
||||||
|
<MessagesList
|
||||||
|
hasScrolled={hasScrolled}
|
||||||
|
setHasScrolled={setHasScrolled}
|
||||||
|
blocked={moderation?.blocked}
|
||||||
|
footer={
|
||||||
|
<MessagesListBlockedFooter
|
||||||
|
recipient={recipient}
|
||||||
|
convoId={convoState.convo.id}
|
||||||
|
hasMessages={convoState.items.length > 0}
|
||||||
|
blockInfo={blockInfo}
|
||||||
|
/>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
@ -65,6 +65,17 @@ function ChatListItemReady({
|
||||||
[profile, moderationOpts],
|
[profile, moderationOpts],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const blockInfo = React.useMemo(() => {
|
||||||
|
const modui = moderation.ui('profileView')
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
}, [moderation])
|
||||||
|
|
||||||
const isDeletedAccount = profile.handle === 'missing.invalid'
|
const isDeletedAccount = profile.handle === 'missing.invalid'
|
||||||
const displayName = isDeletedAccount
|
const displayName = isDeletedAccount
|
||||||
? 'Deleted Account'
|
? 'Deleted Account'
|
||||||
|
@ -241,7 +252,7 @@ function ChatListItemReady({
|
||||||
triggerOpacity={
|
triggerOpacity={
|
||||||
!gtMobile || showActions || menuControl.isOpen ? 1 : 0
|
!gtMobile || showActions || menuControl.isOpen ? 1 : 0
|
||||||
}
|
}
|
||||||
moderation={moderation}
|
blockInfo={blockInfo}
|
||||||
/>
|
/>
|
||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
|
|
Loading…
Reference in New Issue