import {useEffect} from 'react' import {View} from 'react-native' import { ChatBskyGroupDefs, ChatBskyGroupRequestJoin, ChatBskyGroupWithdrawJoinRequest, moderateProfile, } from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useNavigation} from '@react-navigation/native' import {useQueryClient} from '@tanstack/react-query' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {useCallOnce} from '#/lib/once' import {makeProfileLink} from '#/lib/routes/links' import {type NavigationProp} from '#/lib/routes/types' import {isNetworkError} from '#/lib/strings/errors' import {sanitizeHandle} from '#/lib/strings/handles' import {logger} from '#/logger' import {useModerationOpts} from '#/state/preferences/moderation-opts' import { invalidateJoinLinkPreviewsForCode, setJoinLinkPreviewRequestedForCode, useJoinLinkPreviewsQuery, } from '#/state/queries/join-links' import {useRequestJoinGroupChat} from '#/state/queries/messages/request-join-group-chat' import {useWithdrawJoinGroupChatRequest} from '#/state/queries/messages/withdraw-join-group-chat' import {useSession} from '#/state/session' import {atoms as a, useTheme, web} from '#/alf' import {AvatarBubbles} from '#/components/AvatarBubbles' import { Button, type ButtonColor, ButtonIcon, ButtonText, } from '#/components/Button' import * as Dialog from '#/components/Dialog' import {useInteractionState} from '#/components/hooks/useInteractionState' import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' import {ArrowBoxRight_Stroke2_Corner3_Rounded as JoinIcon} from '#/components/icons/ArrowBoxRight' import {ChainLinkBroken_Stroke2_Corner0_Rounded as ChainLinkBrokenIcon} from '#/components/icons/ChainLink' import {PersonGroup_Stroke2_Corner2_Rounded as PersonGroupIcon} from '#/components/icons/Person' import {RaisingHand4Finger_Stroke2_Corner2_Rounded as HandIcon} from '#/components/icons/RaisingHand' import {TimesLarge_Stroke2_Corner0_Rounded as XIcon} from '#/components/icons/Times' import {Warning_Stroke2_Corner0_Rounded as WarningIcon} from '#/components/icons/Warning' import {useIntentDialogs} from '#/components/intents/IntentDialogs' import {InlineLinkText} from '#/components/Link' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {useAnalytics} from '#/analytics' import {ProfileBadges} from '../ProfileBadges' export function GroupChatJoinDialog() { const {groupChatJoinDialogControl, groupChatJoinState} = useIntentDialogs() return ( ) } function GroupChatJoinDialogInner({code}: {code?: string}) { const {t: l} = useLingui() return ( ) } function GroupChatJoinDialogContent({code}: {code?: string}) { const t = useTheme() const {t: l} = useLingui() const {groupChatJoinDialogControl: control} = useIntentDialogs() const {hasSession} = useSession() const moderationOpts = useModerationOpts() const navigation = useNavigation() const queryClient = useQueryClient() const ax = useAnalytics() const logViewOnce = useCallOnce() useEffect(() => { logViewOnce(() => { ax.metric('groupchat:landingPage:view', {hasSession}) }) }, [ax, hasSession, logViewOnce]) const {data, error, isLoading} = useJoinLinkPreviewsQuery({ codes: code ? [code] : undefined, hasSession, staleTime: 0, }) const {mutate: joinGroupChat, isPending: isJoinPending} = useRequestJoinGroupChat({ onSuccess: data => { switch (data.status) { case 'pending': // Optimistically mark the link as requested so any invite cards // backed by the preview cache (e.g. the DM embed) flip to // "Requested" right away, rather than waiting on a server refetch // that can lag behind the write. if (code) setJoinLinkPreviewRequestedForCode(queryClient, code, true) ax.metric('groupchat:inviteLink:redeem', {}) control.close(() => { Toast.show( l`Access requested! The group owner will review your request.`, ) }) break case 'joined': { if (data.convo && data.convo.id) { // Refetch so invite cards pick up the resolved convo and switch // their action to "Open chat". if (code) void invalidateJoinLinkPreviewsForCode(queryClient, code) ax.metric('groupchat:inviteLink:redeem', {}) control.close(() => { Toast.show(l`Successfully joined the group chat!`) navigation.navigate('MessagesConversation', { conversation: data.convo!.id, }) }) } else { logger.warn('Request to join group chat returned no convo ID', { status: data.status, convoId: data.convo?.id, }) } break } } }, onError: error => { let errorMessage = l`Failed to join the group chat. Please try again.` if (isNetworkError(error)) { errorMessage = l`There was a problem with your internet connection, please try again` } else if (error instanceof ChatBskyGroupRequestJoin.ConvoLockedError) { errorMessage = l`This conversation is locked.` } else if ( error instanceof ChatBskyGroupRequestJoin.FollowRequiredError ) { errorMessage = l`Only followers can join this group chat.` } else if (error instanceof ChatBskyGroupRequestJoin.InvalidCodeError) { errorMessage = l`Invalid group chat code.` } else if ( error instanceof ChatBskyGroupRequestJoin.LinkDisabledError ) { errorMessage = l`This invite link has been disabled.` } else if ( error instanceof ChatBskyGroupRequestJoin.MemberLimitReachedError ) { errorMessage = l`The member limit has been reached.` const preview = data?.joinLinkPreviews[0] if ( ChatBskyGroupDefs.isJoinLinkPreviewView(preview) && preview.convo?.id ) { ax.metric('groupchat:join:memberLimitReached', { convoId: preview.convo.id, }) } } else if (error instanceof ChatBskyGroupRequestJoin.UserKickedError) { errorMessage = l`You have been previously removed from this group and can’t join it using this link.` } Toast.show(errorMessage) }, }) const {mutate: withdrawRequest, isPending: isWithdrawPending} = useWithdrawJoinGroupChatRequest({ onSuccess: () => { // Optimistically clear the requested state so invite cards backed by // the preview cache flip back to "Request to join" right away. if (code) setJoinLinkPreviewRequestedForCode(queryClient, code, false) control.close(() => { Toast.show(l`Join request rescinded.`) }) }, onError: error => { let errorMessage = l`Failed to rescind your request. Please try again.` if (isNetworkError(error)) { errorMessage = l`There was a problem with your internet connection, please try again` } else if ( error instanceof ChatBskyGroupWithdrawJoinRequest.InvalidJoinRequestError ) { errorMessage = l`Invalid rescind request.` } Toast.show(errorMessage) }, }) const { state: interacted, onIn: onInteract, onOut: onInteractOut, } = useInteractionState() const handleJoin = () => { if (!code) return joinGroupChat({code}) } const handleWithdraw = () => { if (!convoId) return withdrawRequest({convoId}) } // Fallback if the prefetch exceeds the timeout if (isLoading || !data || !moderationOpts) { return ( ) } if (error) { return ( <> This invite link is invalid ) } const joinLinkPreview = data.joinLinkPreviews[0] if (!ChatBskyGroupDefs.isJoinLinkPreviewView(joinLinkPreview)) { return ( <> Chat invite link no longer available ) } const convoId = joinLinkPreview.convoId const isFollowing = joinLinkPreview.owner.viewer?.followedBy ?? false const hasRequested = !joinLinkPreview.convo && joinLinkPreview.viewer?.requestedAt != null let canJoin = true let ButtonIconImage = isJoinPending || isWithdrawPending ? Loader : JoinIcon let buttonText = joinLinkPreview.requireApproval ? l`Request to join` : l`Join` let buttonColor: ButtonColor = 'primary' if (joinLinkPreview.memberCount >= joinLinkPreview.memberLimit) { canJoin = false ButtonIconImage = HandIcon buttonText = l`This chat is full` buttonColor = 'secondary' } else if (joinLinkPreview.joinRule === 'followedByOwner' && !isFollowing) { canJoin = false ButtonIconImage = HandIcon buttonText = l`Only people the chat owner follows can join` buttonColor = 'secondary' } else if (hasRequested) { ButtonIconImage = XIcon buttonText = l`Rescind request` buttonColor = 'secondary' } return ( <> Group chat {joinLinkPreview.name} {joinLinkPreview.memberCount}/{joinLinkPreview.memberLimit}{' '} members {joinLinkPreview.joinRule === 'followedByOwner' ? l`Followers can join` : l`Anyone can join`} By{' '} { onInteract() }, onMouseLeave: () => { onInteractOut() }, })}> {createSanitizedDisplayName( joinLinkPreview.owner, true, moderateProfile(joinLinkPreview.owner, moderationOpts).ui( 'displayName', ), )} {sanitizeHandle(joinLinkPreview.owner.handle, '@')} {joinLinkPreview.convo ? ( ) : ( )} ) }