[🐴] 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
This commit is contained in:
parent
1b47ea7367
commit
d02e0884c4
13 changed files with 599 additions and 280 deletions
|
@ -23,7 +23,7 @@ import {isWeb} from 'platform/detection'
|
|||
import {List} from 'view/com/util/List'
|
||||
import {MessageInput} from '#/screens/Messages/Conversation/MessageInput'
|
||||
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 {NewMessagesPill} from '#/components/dms/NewMessagesPill'
|
||||
import {Loader} from '#/components/Loader'
|
||||
|
@ -66,12 +66,17 @@ function onScrollToIndexFailed() {
|
|||
export function MessagesList({
|
||||
hasScrolled,
|
||||
setHasScrolled,
|
||||
blocked,
|
||||
footer,
|
||||
}: {
|
||||
hasScrolled: boolean
|
||||
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
|
||||
blocked?: boolean
|
||||
footer?: React.ReactNode
|
||||
}) {
|
||||
const convo = useConvoActive()
|
||||
const convoState = useConvoActive()
|
||||
const {getAgent} = useAgent()
|
||||
|
||||
const flatListRef = useAnimatedRef<FlatList>()
|
||||
|
||||
const [showNewMessagesPill, setShowNewMessagesPill] = React.useState(false)
|
||||
|
@ -81,7 +86,7 @@ export function MessagesList({
|
|||
// the bottom.
|
||||
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)
|
||||
|
||||
// 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 (
|
||||
hasScrolled &&
|
||||
height - contentHeight.value > layoutHeight.value - 50 &&
|
||||
convo.items.length - prevItemCount.current > 1
|
||||
convoState.items.length - prevItemCount.current > 1
|
||||
) {
|
||||
newOffset = contentHeight.value - 50
|
||||
setShowNewMessagesPill(true)
|
||||
} else if (!hasScrolled && !convo.isFetchingHistory) {
|
||||
} else if (!hasScrolled && !convoState.isFetchingHistory) {
|
||||
setHasScrolled(true)
|
||||
}
|
||||
|
||||
|
@ -141,12 +146,12 @@ export function MessagesList({
|
|||
isMomentumScrolling.value = true
|
||||
}
|
||||
contentHeight.value = height
|
||||
prevItemCount.current = convo.items.length
|
||||
prevItemCount.current = convoState.items.length
|
||||
},
|
||||
[
|
||||
hasScrolled,
|
||||
convo.items.length,
|
||||
convo.isFetchingHistory,
|
||||
convoState.items.length,
|
||||
convoState.isFetchingHistory,
|
||||
setHasScrolled,
|
||||
// all of these are stable
|
||||
contentHeight,
|
||||
|
@ -161,9 +166,9 @@ export function MessagesList({
|
|||
|
||||
const onStartReached = useCallback(() => {
|
||||
if (hasScrolled) {
|
||||
convo.fetchMessageHistory()
|
||||
convoState.fetchMessageHistory()
|
||||
}
|
||||
}, [convo, hasScrolled])
|
||||
}, [convoState, hasScrolled])
|
||||
|
||||
const onSendMessage = useCallback(
|
||||
async (text: string) => {
|
||||
|
@ -182,12 +187,12 @@ export function MessagesList({
|
|||
return true
|
||||
})
|
||||
|
||||
convo.sendMessage({
|
||||
convoState.sendMessage({
|
||||
text: rt.text,
|
||||
facets: rt.facets,
|
||||
})
|
||||
},
|
||||
[convo, getAgent],
|
||||
[convoState, getAgent],
|
||||
)
|
||||
|
||||
const onScroll = React.useCallback(
|
||||
|
@ -225,11 +230,9 @@ export function MessagesList({
|
|||
|
||||
// -- Keyboard animation handling
|
||||
const animatedKeyboard = useAnimatedKeyboard()
|
||||
const {gtMobile} = useBreakpoints()
|
||||
const {bottom: bottomInset} = useSafeAreaInsets()
|
||||
const nativeBottomBarHeight = isIOS ? 42 : 60
|
||||
const bottomOffset =
|
||||
isWeb && gtMobile ? 0 : bottomInset + nativeBottomBarHeight
|
||||
const bottomOffset = isWeb ? 0 : bottomInset + nativeBottomBarHeight
|
||||
|
||||
// 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
|
||||
|
@ -268,11 +271,10 @@ export function MessagesList({
|
|||
<ScrollProvider onScroll={onScroll} onMomentumEnd={onMomentumEnd}>
|
||||
<List
|
||||
ref={flatListRef}
|
||||
data={convo.items}
|
||||
data={convoState.items}
|
||||
renderItem={renderItem}
|
||||
keyExtractor={keyExtractor}
|
||||
containWeb={true}
|
||||
contentContainerStyle={[a.px_md]}
|
||||
disableVirtualization={true}
|
||||
// The extra two items account for the header and the footer components
|
||||
initialNumToRender={isNative ? 32 : 62}
|
||||
|
@ -289,14 +291,18 @@ export function MessagesList({
|
|||
onScrollToIndexFailed={onScrollToIndexFailed}
|
||||
scrollEventThrottle={100}
|
||||
ListHeaderComponent={
|
||||
<MaybeLoader isLoading={convo.isFetchingHistory} />
|
||||
<MaybeLoader isLoading={convoState.isFetchingHistory} />
|
||||
}
|
||||
/>
|
||||
</ScrollProvider>
|
||||
<MessageInput
|
||||
onSendMessage={onSendMessage}
|
||||
scrollToEnd={scrollToEndNow}
|
||||
/>
|
||||
{!blocked ? (
|
||||
<MessageInput
|
||||
onSendMessage={onSendMessage}
|
||||
scrollToEnd={scrollToEndNow}
|
||||
/>
|
||||
) : (
|
||||
footer
|
||||
)}
|
||||
{showNewMessagesPill && <NewMessagesPill />}
|
||||
</Animated.View>
|
||||
)
|
||||
|
|
|
@ -1,35 +1,28 @@
|
|||
import React, {useCallback} from 'react'
|
||||
import {TouchableOpacity, View} from 'react-native'
|
||||
import {View} from 'react-native'
|
||||
import {AppBskyActorDefs, moderateProfile, ModerationOpts} from '@atproto/api'
|
||||
import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome'
|
||||
import {msg} from '@lingui/macro'
|
||||
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 {makeProfileLink} from '#/lib/routes/links'
|
||||
import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types'
|
||||
import {CommonNavigatorParams} from '#/lib/routes/types'
|
||||
import {useGate} from '#/lib/statsig/statsig'
|
||||
import {useProfileShadow} from '#/state/cache/profile-shadow'
|
||||
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
||||
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
||||
import {useProfileQuery} from '#/state/queries/profile'
|
||||
import {BACK_HITSLOP} from 'lib/constants'
|
||||
import {sanitizeDisplayName} from 'lib/strings/display-names'
|
||||
import {isWeb} from 'platform/detection'
|
||||
import {useProfileShadow} from 'state/cache/profile-shadow'
|
||||
import {ConvoProvider, isConvoActive, useConvo} from 'state/messages/convo'
|
||||
import {ConvoStatus} from 'state/messages/convo/types'
|
||||
import {useSetMinimalShellMode} from 'state/shell'
|
||||
import {PreviewableUserAvatar} from 'view/com/util/UserAvatar'
|
||||
import {CenteredView} from 'view/com/util/Views'
|
||||
import {MessagesList} from '#/screens/Messages/Conversation/MessagesList'
|
||||
import {atoms as a, useBreakpoints, useTheme, web} from '#/alf'
|
||||
import {ConvoMenu} from '#/components/dms/ConvoMenu'
|
||||
import {atoms as a, useBreakpoints, useTheme} from '#/alf'
|
||||
import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter'
|
||||
import {MessagesListHeader} from '#/components/dms/MessagesListHeader'
|
||||
import {Error} from '#/components/Error'
|
||||
import {Link} from '#/components/Link'
|
||||
import {ListMaybePlaceholder} from '#/components/Lists'
|
||||
import {Loader} from '#/components/Loader'
|
||||
import {Text} from '#/components/Typography'
|
||||
import {ClipClopGate} from '../gate'
|
||||
|
||||
type Props = NativeStackScreenProps<
|
||||
|
@ -73,6 +66,11 @@ function Inner() {
|
|||
const convoState = useConvo()
|
||||
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,
|
||||
// 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.
|
||||
|
@ -86,7 +84,7 @@ function Inner() {
|
|||
if (convoState.status === ConvoStatus.Error) {
|
||||
return (
|
||||
<CenteredView style={a.flex_1} sideBorders>
|
||||
<Header />
|
||||
<MessagesListHeader />
|
||||
<Error
|
||||
title={_(msg`Something went wrong`)}
|
||||
message={_(msg`We couldn't load this conversation`)}
|
||||
|
@ -96,20 +94,21 @@ function Inner() {
|
|||
)
|
||||
}
|
||||
|
||||
/*
|
||||
* Any other convo states (atm) are "ready" states
|
||||
*/
|
||||
return (
|
||||
<CenteredView style={[a.flex_1]} sideBorders>
|
||||
<Header profile={convoState.recipients?.[0]} />
|
||||
{!readyToShow && <MessagesListHeader />}
|
||||
<View style={[a.flex_1]}>
|
||||
{isConvoActive(convoState) ? (
|
||||
<MessagesList
|
||||
{moderationOpts && recipient ? (
|
||||
<InnerReady
|
||||
moderationOpts={moderationOpts}
|
||||
recipient={recipient}
|
||||
hasScrolled={hasScrolled}
|
||||
setHasScrolled={setHasScrolled}
|
||||
/>
|
||||
) : (
|
||||
<ListMaybePlaceholder isLoading />
|
||||
<>
|
||||
<View style={[a.align_center, a.gap_sm, a.flex_1]} />
|
||||
</>
|
||||
)}
|
||||
{!readyToShow && (
|
||||
<View
|
||||
|
@ -132,160 +131,55 @@ function Inner() {
|
|||
)
|
||||
}
|
||||
|
||||
const PFP_SIZE = isWeb ? 40 : 34
|
||||
|
||||
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,
|
||||
function InnerReady({
|
||||
moderationOpts,
|
||||
recipient: recipientUnshadowed,
|
||||
hasScrolled,
|
||||
setHasScrolled,
|
||||
}: {
|
||||
profile: AppBskyActorDefs.ProfileViewBasic
|
||||
moderationOpts: ModerationOpts
|
||||
recipient: AppBskyActorDefs.ProfileViewBasic
|
||||
hasScrolled: boolean
|
||||
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
|
||||
}) {
|
||||
const t = useTheme()
|
||||
const convoState = useConvo()
|
||||
const profile = useProfileShadow(profileUnshadowed)
|
||||
const moderation = React.useMemo(
|
||||
() => moderateProfile(profile, moderationOpts),
|
||||
[profile, moderationOpts],
|
||||
)
|
||||
const recipient = useProfileShadow(recipientUnshadowed)
|
||||
|
||||
const isDeletedAccount = profile?.handle === 'missing.invalid'
|
||||
const displayName = isDeletedAccount
|
||||
? 'Deleted Account'
|
||||
: sanitizeDisplayName(
|
||||
profile.displayName || profile.handle,
|
||||
moderation.ui('displayName'),
|
||||
)
|
||||
const moderation = React.useMemo(() => {
|
||||
return moderateProfile(recipient, moderationOpts)
|
||||
}, [recipient, 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])
|
||||
|
||||
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>
|
||||
|
||||
<MessagesListHeader
|
||||
profile={recipient}
|
||||
moderation={moderation}
|
||||
blockInfo={blockInfo}
|
||||
/>
|
||||
{isConvoActive(convoState) && (
|
||||
<ConvoMenu
|
||||
convo={convoState.convo}
|
||||
profile={profile}
|
||||
currentScreen="conversation"
|
||||
moderation={moderation}
|
||||
<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],
|
||||
)
|
||||
|
||||
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 displayName = isDeletedAccount
|
||||
? 'Deleted Account'
|
||||
|
@ -241,7 +252,7 @@ function ChatListItemReady({
|
|||
triggerOpacity={
|
||||
!gtMobile || showActions || menuControl.isOpen ? 1 : 0
|
||||
}
|
||||
moderation={moderation}
|
||||
blockInfo={blockInfo}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue