import React, {useCallback} from 'react' import {TouchableOpacity, View} from 'react-native' import {KeyboardProvider} from 'react-native-keyboard-controller' import {KeyboardAvoidingView} from 'react-native-keyboard-controller' import {useSafeAreaInsets} from 'react-native-safe-area-context' import {AppBskyActorDefs, 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 {NativeStackScreenProps} from '@react-navigation/native-stack' import {CommonNavigatorParams, NavigationProp} from '#/lib/routes/types' import {useGate} from '#/lib/statsig/statsig' import {useProfileShadow} from '#/state/cache/profile-shadow' import {useCurrentConvoId} from '#/state/messages/current-convo-id' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useProfileQuery} from '#/state/queries/profile' import {BACK_HITSLOP} from 'lib/constants' import {sanitizeDisplayName} from 'lib/strings/display-names' import {isIOS, isNative, isWeb} from 'platform/detection' 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} from '#/alf' import {ConvoMenu} from '#/components/dms/ConvoMenu' import {Error} from '#/components/Error' import {ListMaybePlaceholder} from '#/components/Lists' import {Loader} from '#/components/Loader' import {Text} from '#/components/Typography' import {ClipClopGate} from '../gate' type Props = NativeStackScreenProps< CommonNavigatorParams, 'MessagesConversation' > export function MessagesConversationScreen({route}: Props) { const gate = useGate() const setMinimalShellMode = useSetMinimalShellMode() const {gtMobile} = useBreakpoints() const convoId = route.params.conversation const {setCurrentConvoId} = useCurrentConvoId() useFocusEffect( useCallback(() => { setCurrentConvoId(convoId) if (isWeb && !gtMobile) { setMinimalShellMode(true) } return () => { setCurrentConvoId(undefined) setMinimalShellMode(false) } }, [convoId, gtMobile, setCurrentConvoId, setMinimalShellMode]), ) if (!gate('dms')) return return ( ) } function Inner() { const t = useTheme() const convoState = useConvo() const {_} = useLingui() const [hasInitiallyRendered, setHasInitiallyRendered] = React.useState(false) const {bottom: bottomInset, top: topInset} = useSafeAreaInsets() const nativeBottomBarHeight = isIOS ? 42 : 60 // HACK: Because we need to scroll to the bottom of the list once initial items are added to the list, we also have // to take into account that scrolling to the end of the list on native will happen asynchronously. This will cause // a little flicker when the items are first renedered at the top and immediately scrolled to the bottom. to prevent // this, we will wait until the first render has completed to remove the loading overlay. React.useEffect(() => { if ( !hasInitiallyRendered && isConvoActive(convoState) && !convoState.isFetchingHistory ) { setTimeout(() => { setHasInitiallyRendered(true) }, 15) } }, [convoState, hasInitiallyRendered]) if (convoState.status === ConvoStatus.Error) { return (
convoState.error.retry()} /> ) } /* * Any other convo states (atm) are "ready" states */ return (
{isConvoActive(convoState) ? ( ) : ( )} {!hasInitiallyRendered && ( )} ) } let Header = ({ profile: initialProfile, }: { profile?: AppBskyActorDefs.ProfileViewBasic }): React.ReactNode => { const t = useTheme() const {_} = useLingui() const {gtTablet} = useBreakpoints() const navigation = useNavigation() const moderationOpts = useModerationOpts() const {data: profile} = useProfileQuery({did: initialProfile?.did}) const onPressBack = useCallback(() => { if (isWeb) { navigation.replace('Messages') } else { navigation.goBack() } }, [navigation]) return ( {!gtTablet ? ( ) : ( )} {profile && moderationOpts ? ( ) : ( <> )} ) } Header = React.memo(Header) function HeaderReady({ profile: profileUnshadowed, moderationOpts, }: { profile: AppBskyActorDefs.ProfileViewBasic moderationOpts: ModerationOpts }) { const t = useTheme() const convoState = useConvo() const profile = useProfileShadow(profileUnshadowed) const moderation = React.useMemo( () => moderateProfile(profile, moderationOpts), [profile, moderationOpts], ) const isDeletedAccount = profile?.handle === 'missing.invalid' const displayName = isDeletedAccount ? 'Deleted Account' : sanitizeDisplayName( profile.displayName || profile.handle, moderation.ui('displayName'), ) return ( <> {displayName} {!isDeletedAccount && ( @{profile.handle} )} {isConvoActive(convoState) && ( )} ) }