import {useEffect, useState} from 'react' import {View} from 'react-native' import {moderateProfile, type ModerationOpts} from '@atproto/api' import {Trans, useLingui} from '@lingui/react/macro' import {useOpenComposer} from '#/lib/hooks/useOpenComposer' import {createSanitizedDisplayName} from '#/lib/moderation/create-sanitized-display-name' import {shareUrl} from '#/lib/sharing' import {useCreateJoinLink} from '#/state/queries/messages/create-join-link' import {useDisableJoinLink} from '#/state/queries/messages/disable-join-link' import {useEditJoinLink} from '#/state/queries/messages/edit-join-link' import {useEnableJoinLink} from '#/state/queries/messages/enable-join-link' import {atoms as a, useTheme, web} from '#/alf' import { Button, ButtonIcon, ButtonText, StackedButton, } from '#/components/Button' import * as Dialog from '#/components/Dialog' import { type ConvoWithDetails, type GroupConvoMember, } from '#/components/dms/util' import * as Toggle from '#/components/forms/Toggle' import {ArrowRight_Stroke2_Corner0_Rounded as ArrowRightIcon} from '#/components/icons/Arrow' import {ArrowShareRight_Stroke2_Corner2_Rounded as ArrowShareRightIcon} from '#/components/icons/ArrowShareRight' import {ChainLinkBroken_Stroke2_Corner0_Rounded as ChainLinkBrokenIcon} from '#/components/icons/ChainLink' import {EditBig_Stroke2_Corner2_Rounded as EditIcon} from '#/components/icons/EditBig' import {Loader} from '#/components/Loader' import * as Toast from '#/components/Toast' import {Text} from '#/components/Typography' import {IS_WEB} from '#/env' import {CopyTextButton} from './CopyTextButton' import {EditTextButton} from './EditTextButton' enum Step { INFO, GENERATE, MANAGE, CONFIRM_DISABLE, } export function InviteLinkDialog({ convo, control, owner, isOwner, moderationOpts, }: { convo: Extract control: Dialog.DialogOuterProps['control'] owner: GroupConvoMember isOwner: boolean moderationOpts: ModerationOpts }) { const t = useTheme() const {t: l, i18n} = useLingui() const ownerName = createSanitizedDisplayName( owner, false, moderateProfile(owner, moderationOpts).ui('displayName'), ) const {joinLink} = convo.details const enabledStatus = joinLink?.enabledStatus const defaultStep = joinLink ? Step.MANAGE : Step.INFO const defaultWhoCanJoin = joinLink ? [ `${joinLink.joinRule}${joinLink.requireApproval ? ':requireApproval' : ''}`, ] : ['anyone'] const [step, setStep] = useState(defaultStep) const [whoCanJoin, setWhoCanJoin] = useState(defaultWhoCanJoin) // Resync local state when the server-side join link rules change (mutation // success, refetch, or change from another client). Keyed on the rule string // so identity-only refetches don't bump a user mid-edit. const joinLinkRuleKey = joinLink ? `${joinLink.joinRule}${joinLink.requireApproval ? ':requireApproval' : ''}` : null useEffect(() => { setStep(joinLinkRuleKey ? Step.MANAGE : Step.INFO) setWhoCanJoin([joinLinkRuleKey ?? 'anyone']) }, [joinLinkRuleKey]) const {openComposer} = useOpenComposer() const {mutate: createJoinLink, isPending: isCreating} = useCreateJoinLink( convo.view.id, { onSuccess: () => { setStep(Step.MANAGE) }, onError: () => { Toast.show(l`Failed to create invite link`, { type: 'error', }) }, }, ) const {mutate: editJoinLink, isPending: isEditing} = useEditJoinLink( convo.view.id, { onSuccess: () => { setStep(Step.MANAGE) }, onError: () => { Toast.show(l`Failed to edit invite link`, { type: 'error', }) }, }, ) const {mutate: disableJoinLink, isPending: isDisabling} = useDisableJoinLink( convo.view.id, { onError: () => { Toast.show(l`Failed to disable invite link`, { type: 'error', }) }, }, ) const {mutate: enableJoinLink, isPending: isEnabling} = useEnableJoinLink( convo.view.id, { onError: () => { Toast.show(l`Failed to enable invite link`, { type: 'error', }) }, }, ) const isSaving = isCreating || isEditing const whoCanJoinOptions = [ { name: 'anyone', owner: l`Anyone can join instantly`, member: l`Anyone can join instantly`, }, { name: 'anyone:requireApproval', owner: l`Anyone can request to join`, member: l`Anyone can request to join`, }, { name: 'followedByOwner', owner: l`People I follow can join instantly`, member: l`People ${ownerName} follows can join instantly`, }, { name: 'followedByOwner:requireApproval', owner: l`People I follow can request to join`, member: l`People ${ownerName} follows can request to join`, }, ] let content: React.ReactNode = null let header: string | null = null switch (step) { case Step.INFO: header = l`Invite link` content = ( <> An invite link lets people join this group chat without being added directly. You control who can use the link and whether they need your approval. You can disable the link at any time. Your name, avatar, and the name of the group chat will be visible to everyone. ) break case Step.GENERATE: header = joinLink && enabledStatus === 'enabled' ? l`Update invite link` : l`Generate invite link` content = ( <> Choose who can join this group chat and how. {whoCanJoinOptions.map(option => ( {({selected}) => ( )} ))} ) break case Step.MANAGE: { const joinLinkURI = joinLink?.code ? `https://bsky.app/chat/${joinLink.code}` : 'https://bsky.app/chat' const createdAt = joinLink ? new Date(joinLink.createdAt) : null const currentOptionName = joinLink ? `${joinLink.joinRule}${joinLink.requireApproval ? ':requireApproval' : ''}` : whoCanJoinOptions[0].name const currentOption = whoCanJoinOptions.find( o => o.name === currentOptionName, ) const ownerValue = currentOption?.owner ?? whoCanJoinOptions[0].owner const memberValue = currentOption?.member ?? whoCanJoinOptions[0].member header = enabledStatus === 'enabled' ? l`Invite link` : l`Invite link disabled` content = ( <> {joinLinkURI} {createdAt ? ( Created{' '} {i18n.date(createdAt, { dateStyle: 'long', timeStyle: 'short', })} ) : null} {enabledStatus === 'enabled' ? ( {isOwner ? ( setStep(Step.GENERATE)}> {ownerValue} ) : ( {memberValue} )} ) : null} {enabledStatus === 'enabled' ? ( {isOwner ? ( setStep(Step.CONFIRM_DISABLE)}> Disable ) : null} { control.close(() => { openComposer({ text: joinLinkURI, logContext: 'Other', }) }) }}> Post link { void shareUrl(joinLinkURI) }}> Share ) : ( )} ) break } case Step.CONFIRM_DISABLE: content = ( <> Disable this invite link? Anyone who has it will no longer be able to join or request to join. You can always create a new one. ) break } if (!isOwner && (!joinLink || joinLink.enabledStatus === 'disabled')) { header = l`Invite link` content = ( <> There is no invite link for this group chat. ) } return ( { setStep(defaultStep) setWhoCanJoin(defaultWhoCanJoin) }}> {header} } label={l`Group chat invite link dialog`} style={web({maxWidth: 400})}> {content} ) }