302 lines
9.4 KiB
TypeScript
302 lines
9.4 KiB
TypeScript
import {useCallback, useEffect, useMemo, useState} from 'react'
|
|
import {type LayoutChangeEvent, View} from 'react-native'
|
|
import {useSafeAreaInsets} from 'react-native-safe-area-context'
|
|
import {ChatBskyConvoDefs, moderateProfile} from '@atproto/api'
|
|
import {
|
|
ScrollEdgeEffect,
|
|
ScrollEdgeEffectProvider,
|
|
} from '@bsky.app/expo-scroll-edge-effect'
|
|
import {Trans, useLingui} from '@lingui/react/macro'
|
|
import {
|
|
type RouteProp,
|
|
useFocusEffect,
|
|
useIsFocused,
|
|
useNavigation,
|
|
useRoute,
|
|
} from '@react-navigation/native'
|
|
import {type NativeStackScreenProps} from '@react-navigation/native-stack'
|
|
|
|
import {useNonReactiveCallback} from '#/lib/hooks/useNonReactiveCallback'
|
|
import {useViewportZoomLock} from '#/lib/hooks/useViewportZoomLock'
|
|
import {
|
|
type CommonNavigatorParams,
|
|
type NavigationProp,
|
|
} from '#/lib/routes/types'
|
|
import {useMaybeProfileShadow} from '#/state/cache/profile-shadow'
|
|
import {useEmail} from '#/state/email-verification'
|
|
import {ConvoProvider, isConvoActive, useConvo} from '#/state/messages/convo'
|
|
import {ConvoStatus} from '#/state/messages/convo/types'
|
|
import {useCurrentConvoId} from '#/state/messages/current-convo-id'
|
|
import {useModerationOpts} from '#/state/preferences/moderation-opts'
|
|
import {useConvoQuery} from '#/state/queries/messages/conversation'
|
|
import {useMarkJoinRequestsRead} from '#/state/queries/messages/mark-join-request-read'
|
|
import {useSession} from '#/state/session'
|
|
import {MessagesList} from '#/screens/Messages/components/MessagesList'
|
|
import {atoms as a, web} from '#/alf'
|
|
import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen'
|
|
import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy'
|
|
import {
|
|
EmailDialogScreenID,
|
|
useEmailDialogControl,
|
|
} from '#/components/dialogs/EmailDialog'
|
|
import {MessagesListBlockedFooter} from '#/components/dms/MessagesListBlockedFooter'
|
|
import {MessagesListHeader} from '#/components/dms/MessagesListHeader'
|
|
import {type ConvoWithDetails, parseConvoView} from '#/components/dms/util'
|
|
import {Error} from '#/components/Error'
|
|
import * as Layout from '#/components/Layout'
|
|
import {IS_LIQUID_GLASS} from '#/env'
|
|
import {ChatDisabled} from './components/ChatDisabled'
|
|
import {ChatEnded} from './components/ChatEnded'
|
|
import {ChatLocked} from './components/ChatLocked'
|
|
import {RequestStatus} from './components/RequestStatus'
|
|
|
|
type Props = NativeStackScreenProps<
|
|
CommonNavigatorParams,
|
|
'MessagesConversation'
|
|
>
|
|
|
|
export function MessagesConversationScreen(props: Props) {
|
|
const {t: l} = useLingui()
|
|
const aaCopy = useAgeAssuranceCopy()
|
|
return (
|
|
<AgeRestrictedScreen
|
|
screenTitle={l`Conversation`}
|
|
infoText={aaCopy.chatsInfoText}>
|
|
<MessagesConversationScreenInner {...props} />
|
|
</AgeRestrictedScreen>
|
|
)
|
|
}
|
|
|
|
export function MessagesConversationScreenInner({route}: Props) {
|
|
const convoId = route.params.conversation
|
|
const {setCurrentConvoId} = useCurrentConvoId()
|
|
|
|
useFocusEffect(
|
|
useCallback(() => {
|
|
setCurrentConvoId(convoId)
|
|
|
|
return () => {
|
|
setCurrentConvoId(undefined)
|
|
}
|
|
}, [convoId, setCurrentConvoId]),
|
|
)
|
|
|
|
return (
|
|
<Layout.Screen
|
|
minimalShell
|
|
testID="convoScreen"
|
|
noInsetTop={IS_LIQUID_GLASS}
|
|
style={web([{minHeight: 0}, a.flex_1])}>
|
|
<ScrollEdgeEffectProvider>
|
|
<ConvoProvider key={convoId} convoId={convoId}>
|
|
<Inner convoId={convoId} />
|
|
</ConvoProvider>
|
|
</ScrollEdgeEffectProvider>
|
|
</Layout.Screen>
|
|
)
|
|
}
|
|
|
|
function Inner({convoId}: {convoId: string}) {
|
|
const convoState = useConvo()
|
|
const {t: l} = useLingui()
|
|
const {currentAccount} = useSession()
|
|
const isFocused = useIsFocused()
|
|
const {top: topInset} = useSafeAreaInsets()
|
|
const {data: convoData} = useConvoQuery({convoId})
|
|
|
|
useViewportZoomLock({enabled: isFocused})
|
|
|
|
const convo = convoData
|
|
? parseConvoView(convoData, currentAccount?.did)
|
|
: null
|
|
|
|
const [hasScrolled, setHasScrolled] = useState(false)
|
|
|
|
// Any time that we re-render the `Initializing` state, we have to reset `hasScrolled` to false. After entering this
|
|
// state, we know that we're resetting the list of messages and need to re-scroll to the bottom when they get added.
|
|
const [prevState, setPrevState] = useState(convoState.status)
|
|
if (prevState !== convoState.status) {
|
|
setPrevState(convoState.status)
|
|
if (convoState.status === ConvoStatus.Initializing) {
|
|
setHasScrolled(false)
|
|
}
|
|
}
|
|
|
|
if (convoState.status === ConvoStatus.Error) {
|
|
return (
|
|
<>
|
|
<Layout.Center
|
|
style={[a.w_full, IS_LIQUID_GLASS && {paddingTop: topInset}]}>
|
|
<MessagesListHeader convo={convo} />
|
|
</Layout.Center>
|
|
<Error
|
|
title={l`Something went wrong`}
|
|
message={l`We couldn't load this conversation`}
|
|
onRetry={() => convoState.error.retry()}
|
|
/>
|
|
</>
|
|
)
|
|
}
|
|
|
|
return (
|
|
<Layout.Center style={[a.flex_1]}>
|
|
<View style={[a.flex_1]}>
|
|
<InnerReady
|
|
convo={convo}
|
|
hasScrolled={hasScrolled}
|
|
setHasScrolled={setHasScrolled}
|
|
isActive={isConvoActive(convoState)}
|
|
isDisabled={convoState.status === ConvoStatus.Disabled}
|
|
/>
|
|
</View>
|
|
</Layout.Center>
|
|
)
|
|
}
|
|
|
|
function InnerReady({
|
|
hasScrolled,
|
|
setHasScrolled,
|
|
convo,
|
|
isActive,
|
|
isDisabled,
|
|
}: {
|
|
hasScrolled: boolean
|
|
setHasScrolled: React.Dispatch<React.SetStateAction<boolean>>
|
|
convo: ConvoWithDetails | null
|
|
isActive: boolean
|
|
isDisabled: boolean
|
|
}) {
|
|
const navigation = useNavigation<NavigationProp>()
|
|
const {top: topInset} = useSafeAreaInsets()
|
|
const [headerHeight, setHeaderHeight] = useState(0)
|
|
const onHeaderLayout = (e: LayoutChangeEvent) => {
|
|
setHeaderHeight(e.nativeEvent.layout.height)
|
|
}
|
|
const {params} =
|
|
useRoute<RouteProp<CommonNavigatorParams, 'MessagesConversation'>>()
|
|
const {needsEmailVerification} = useEmail()
|
|
const emailDialogControl = useEmailDialogControl()
|
|
|
|
const unreadRequestCount =
|
|
convo?.kind === 'group' && ChatBskyConvoDefs.isGroupConvo(convo.view.kind)
|
|
? (convo.view.kind.unreadJoinRequestCount ?? 0)
|
|
: 0
|
|
const {mutate: markJoinRequestsRead} = useMarkJoinRequestsRead(convo?.view.id)
|
|
|
|
/**
|
|
* Must be non-reactive, otherwise the update to open the global dialog will
|
|
* cause a re-render loop.
|
|
*/
|
|
const maybeBlockForEmailVerification = useNonReactiveCallback(() => {
|
|
if (needsEmailVerification) {
|
|
/*
|
|
* HACKFIX
|
|
*
|
|
* Load bearing timeout, to bump this state update until the after the
|
|
* `navigator.addListener('state')` handler closes elements from
|
|
* `shell/index.*.tsx` - sfn & esb
|
|
*/
|
|
setTimeout(() =>
|
|
emailDialogControl.open({
|
|
id: EmailDialogScreenID.Verify,
|
|
instructions: [
|
|
<Trans key="pre-compose">
|
|
Before you can message another user, you must first verify your
|
|
email.
|
|
</Trans>,
|
|
],
|
|
onCloseWithoutVerifying: () => {
|
|
if (navigation.canGoBack()) {
|
|
navigation.goBack()
|
|
} else {
|
|
navigation.navigate('Messages', {animation: 'pop'})
|
|
}
|
|
},
|
|
}),
|
|
)
|
|
}
|
|
})
|
|
|
|
useEffect(() => {
|
|
maybeBlockForEmailVerification()
|
|
}, [maybeBlockForEmailVerification])
|
|
|
|
const primaryMember = useMaybeProfileShadow(convo?.primaryMember)
|
|
const moderationOpts = useModerationOpts()
|
|
const primaryMemberModeration = useMemo(() => {
|
|
if (!primaryMember || !moderationOpts) return null
|
|
return moderateProfile(primaryMember, moderationOpts)
|
|
}, [primaryMember, moderationOpts])
|
|
|
|
const header = <MessagesListHeader convo={convo} />
|
|
|
|
let footer: React.ReactNode = null
|
|
if (isDisabled) {
|
|
footer = <ChatDisabled />
|
|
} else if (
|
|
convo &&
|
|
primaryMember &&
|
|
primaryMemberModeration &&
|
|
(convo.kind === 'group'
|
|
? primaryMemberModeration?.blockCause?.type === 'blocking'
|
|
: primaryMemberModeration?.blocked)
|
|
) {
|
|
footer = (
|
|
<MessagesListBlockedFooter
|
|
recipient={primaryMember}
|
|
convoId={convo.view.id}
|
|
isGroup={convo.kind === 'group'}
|
|
moderation={primaryMemberModeration}
|
|
/>
|
|
)
|
|
} else if (convo?.kind === 'group') {
|
|
if (convo.details.lockStatus === 'locked') {
|
|
footer = <ChatLocked convo={convo} />
|
|
} else if (convo.details.lockStatus === 'locked-permanently') {
|
|
footer = <ChatEnded convo={convo} />
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
{IS_LIQUID_GLASS ? (
|
|
<ScrollEdgeEffect
|
|
edge="top"
|
|
effect="soft"
|
|
style={[a.absolute, a.w_full, a.z_10, {paddingTop: topInset}]}
|
|
onLayout={onHeaderLayout}>
|
|
{header}
|
|
</ScrollEdgeEffect>
|
|
) : (
|
|
<View onLayout={onHeaderLayout}>{header}</View>
|
|
)}
|
|
|
|
{isActive && convo?.kind === 'group' && unreadRequestCount > 0 ? (
|
|
<RequestStatus
|
|
top={headerHeight}
|
|
count={unreadRequestCount}
|
|
onDismiss={() => {
|
|
markJoinRequestsRead()
|
|
}}
|
|
onPress={() => {
|
|
markJoinRequestsRead()
|
|
navigation.navigate('MessagesJoinRequests', {
|
|
conversation: convo.view.id,
|
|
})
|
|
}}
|
|
/>
|
|
) : null}
|
|
|
|
{isActive && (
|
|
<MessagesList
|
|
hasScrolled={hasScrolled}
|
|
setHasScrolled={setHasScrolled}
|
|
hasAcceptOverride={!!params.accept}
|
|
transparentHeaderHeight={IS_LIQUID_GLASS ? headerHeight : 0}
|
|
footer={footer}
|
|
/>
|
|
)}
|
|
</>
|
|
)
|
|
}
|