import {useState} from 'react' import {View} from 'react-native' import { ChatBskyGroupApproveJoinRequest, type ChatBskyGroupListJoinRequests, ChatBskyGroupRejectJoinRequest, } from '@atproto/api' import {Plural, Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {type InfiniteData, useQueryClient} from '@tanstack/react-query' import {useBottomBarOffset} from '#/lib/hooks/useBottomBarOffset' import {isNetworkError} from '#/lib/hooks/useCleanError' import { type CommonNavigatorParams, type NativeStackScreenProps, type NavigationProp, } from '#/lib/routes/types' import {logger} from '#/logger' import {ConvoProvider, useConvo} from '#/state/messages/convo' import {ConvoStatus} from '#/state/messages/convo/types' import {useModerationOpts} from '#/state/preferences/moderation-opts' import {useJoinRequestMutation} from '#/state/queries/messages/join-requests' import { createListJoinRequestsQueryKey, useListJoinRequestsQuery, } from '#/state/queries/messages/list-join-requests' import {useSession} from '#/state/session' import {List} from '#/view/com/util/List' import {atoms as a, useTheme} from '#/alf' import {AgeRestrictedScreen} from '#/components/ageAssurance/AgeRestrictedScreen' import {useAgeAssuranceCopy} from '#/components/ageAssurance/useAgeAssuranceCopy' import {Button, ButtonIcon, ButtonText} from '#/components/Button' import * as Dialog from '#/components/Dialog' import {type ConvoWithDetails} from '#/components/dms/util' import {Error} from '#/components/Error' import {ArrowRotateCounterClockwise_Stroke2_Corner0_Rounded as RetryIcon} from '#/components/icons/ArrowRotate' import {CircleInfo_Stroke2_Corner0_Rounded as ErrorIcon} from '#/components/icons/CircleInfo' import {KnownFollowers} from '#/components/KnownFollowers' import * as Layout from '#/components/Layout' import {Loader} from '#/components/Loader' import * as ProfileCard from '#/components/ProfileCard' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import type * as bsky from '#/types/bsky' import {InviteLinkDialog} from './components/InviteLinkDialog' type Props = NativeStackScreenProps< CommonNavigatorParams, 'MessagesJoinRequests' > export function MessagesJoinRequestsScreen(props: Props) { const {t: l} = useLingui() const aaCopy = useAgeAssuranceCopy() return ( ) } function MessagesJoinRequestsScreenInner({route}: Props) { const convoId = route.params.conversation return ( ) } function JoinRequestsInner() { const {t: l} = useLingui() const convoState = useConvo() const navigation = useNavigation() if (convoState.status === ConvoStatus.Error) { return ( <>
convoState.error.retry()} /> ) } if (!convoState.convo) { return ( <>
) } if (convoState.convo.kind !== 'group') { return ( { if (navigation.canGoBack()) { navigation.goBack() } else { navigation.replace('Messages', {animation: 'pop'}) } }} /> ) } return } function JoinRequestsList({ convo, }: { convo: Extract }) { const t = useTheme() const {t: l} = useLingui() const moderationOpts = useModerationOpts() const bottomBarOffset = useBottomBarOffset() const {currentAccount} = useSession() const navigation = useNavigation() const queryClient = useQueryClient() const inviteLinkControl = Dialog.useDialogControl() const getRemainingRequestCount = () => { const data = queryClient.getQueryData< InfiniteData >(createListJoinRequestsQueryKey({convoId: convo.view.id})) return data?.pages.reduce((sum, page) => sum + page.requests.length, 0) ?? 0 } const [isPTRing, setIsPTRing] = useState(false) const [footerHeight, setFooterHeight] = useState(0) const owner = convo.primaryMember const isOwner = !!owner && owner.did === currentAccount?.did const { data: joinRequestsData, isPending, isError, hasNextPage, fetchNextPage, isFetchingNextPage, refetch, } = useListJoinRequestsQuery({ convoId: convo.view.id, }) const items = joinRequestsData?.pages.flatMap(page => page.requests.map(request => request.requestedBy), ) ?? [] const requestCount = joinRequestsData?.pages.reduce( (sum, page) => sum + page.requests.length, 0, ) ?? 0 const {mutate: approveJoinRequest, isPending: isApprovePending} = useJoinRequestMutation('approve', convo.view.id, { onSuccess: () => { Toast.show(l`Request approved.`) if (getRemainingRequestCount() < 1) { navigation.replace('MessagesConversationSettings', { conversation: convo.view.id, }) } }, onError: error => { let errorMessage = l`Failed to accept join request` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` } else if ( error instanceof ChatBskyGroupApproveJoinRequest.InvalidConvoError ) { errorMessage = l`Conversation not found.` } else if ( error instanceof ChatBskyGroupApproveJoinRequest.InsufficientRoleError ) { errorMessage = l`Only admins can accept join requests.` } else if ( error instanceof ChatBskyGroupApproveJoinRequest.MemberLimitReachedError ) { errorMessage = l`The member limit has been reached.` } Toast.show(errorMessage, {type: 'error'}) }, }) const {mutate: rejectJoinRequest, isPending: isRejectPending} = useJoinRequestMutation('reject', convo.view.id, { onSuccess: () => { Toast.show(l`Request ignored.`) if (getRemainingRequestCount() < 1) { navigation.replace('MessagesConversationSettings', { conversation: convo.view.id, }) } }, onError: error => { let errorMessage = l`Failed to ignore join request` if (isNetworkError(error)) { errorMessage = l`A network error occurred. Please check your internet connection.` } else if ( error instanceof ChatBskyGroupRejectJoinRequest.InvalidConvoError ) { errorMessage = l`Conversation not found.` } else if ( error instanceof ChatBskyGroupRejectJoinRequest.InsufficientRoleError ) { errorMessage = l`Only admins can ignore join requests.` } Toast.show(errorMessage, {type: 'error'}) }, }) const isMutating = isApprovePending || isRejectPending const renderItem = ({item}: {item: bsky.profile.AnyProfileView}) => { if (!moderationOpts) return null return ( approveJoinRequest({member: item.did})} /> rejectJoinRequest({member: item.did})} /> ) } const footer = ( setFooterHeight(evt.nativeEvent.layout.height)} style={[ a.absolute, a.left_0, a.right_0, {bottom: 0}, a.px_xl, a.border_t, t.atoms.bg, t.atoms.border_contrast_low, { paddingTop: a.py_lg.paddingTop, paddingBottom: a.py_lg.paddingBottom + bottomBarOffset, }, ]}> ) const onEndReached = async () => { if (isFetchingNextPage || !hasNextPage || isError) return try { await fetchNextPage() } catch (err) { logger.error('Failed to load more join requests', {message: err}) } } const onRefresh = async () => { setIsPTRing(true) try { await refetch() } catch (err) { logger.error('Failed to refresh group chat requests', {message: err}) } setIsPTRing(false) } if (isError) { return ( <>
Unable to fetch join requests. ) } const showFooter = isOwner return ( <>
item.did} renderItem={renderItem} ListEmptyComponent={ isPending ? ( ) : null } contentContainerStyle={ showFooter ? {paddingBottom: footerHeight} : undefined } scrollIndicatorInsets={showFooter ? {bottom: footerHeight} : undefined} refreshing={isPTRing} onEndReached={() => void onEndReached()} onRefresh={() => void onRefresh()} keyboardDismissMode="on-drag" sideBorders={false} desktopFixedHeight /> {showFooter ? footer : null} {owner && moderationOpts && ( )} ) } function Header({ count, hasMoreRequests, }: { count?: number hasMoreRequests?: boolean }) { return ( {count === undefined ? ( Requests to join ) : hasMoreRequests ? ( ) : ( )} ) } function AcceptButton({ disabled, onPress, }: { disabled?: boolean onPress: () => void }) { const {t: l} = useLingui() return ( ) } function RejectButton({ disabled, onPress, }: { disabled?: boolean onPress: () => void }) { const {t: l} = useLingui() return ( ) }