diff --git a/desktop/src/app/App.tsx b/desktop/src/app/App.tsx index 0fe740ebc3..782cf1c14c 100644 --- a/desktop/src/app/App.tsx +++ b/desktop/src/app/App.tsx @@ -39,7 +39,6 @@ import { WelcomeSetup } from "@/features/communities/ui/WelcomeSetup"; import { CommunityApplyErrorScreen } from "@/features/communities/ui/CommunityApplyErrorScreen"; import { CommunityChangeOverlay } from "@/features/communities/ui/CommunityChangeOverlay"; import { createBuzzQueryClient } from "@/shared/api/queryClient"; -import { getMyRelayMembershipLookup } from "@/shared/api/relayMembers"; import { isSharedIdentity as isSharedIdentityCmd } from "@/shared/api/tauri"; import { type AddCommunityDeepLinkPayload, @@ -62,16 +61,6 @@ const BOOT_SPLASH_MIN_VISIBLE_MS = 1_200; const BOOT_SPLASH_FADE_MS = 200; const INITIAL_RENDER_READY_EVENT = "initial-render-ready"; -function isRelayMembershipDeniedError(error: unknown): boolean { - if (!(error instanceof Error)) return false; - return [ - "You must be a relay member", - "relay_membership_required", - "restricted: not a relay member", - "invalid: you are not a relay member", - ].some((message) => error.message.includes(message)); -} - type BootSplashPhase = "holding" | "fading" | "done"; function useInitialRenderReady() { @@ -275,13 +264,19 @@ function CommunityApp({ }) { const { activeCommunity, + communities, reinitKey, addCommunity, + clearCommunities, + removeCommunity, switchCommunity, reconnectCommunity, } = useCommunities(); const communityOnboarding = useCommunityOnboarding(); + const connectingTransactionRef = useRef(null); const [isCommunityChangeOpen, setIsCommunityChangeOpen] = useState(false); + const [resumeFirstCommunityJoin, setResumeFirstCommunityJoin] = + useState(false); // Surface nest-related backend events (repos-dir errors, legacy migration) // as toasts. Mounted before useCommunityInit so the listeners are registered @@ -311,10 +306,16 @@ function CommunityApp({ const handleCommunityOnboardingConnect = useCallback(() => { const transaction = communityOnboarding.transaction; if (transaction?.stage !== "connecting") return; + if (connectingTransactionRef.current === transaction.id) return; + connectingTransactionRef.current = transaction.id; if (transaction.communityId) { switchCommunity(transaction.communityId); return; } + const previousCommunityId = activeCommunity?.id; + const relayAlreadyExists = communities.some( + (community) => community.relayUrl === transaction.relayUrl, + ); const id = addCommunity({ id: crypto.randomUUID(), name: transaction.communityName, @@ -323,58 +324,70 @@ function CommunityApp({ reposDir: transaction.reposDir, addedAt: new Date().toISOString(), }); - communityOnboarding.update({ communityId: id, error: undefined }); + communityOnboarding.update({ + communityId: id, + previousCommunityId, + addedCommunity: !relayAlreadyExists, + error: undefined, + }); switchCommunity(id); reconnectCommunity(); - }, [addCommunity, communityOnboarding, reconnectCommunity, switchCommunity]); + }, [ + activeCommunity?.id, + addCommunity, + communities, + communityOnboarding, + reconnectCommunity, + switchCommunity, + ]); + + const handleCommunityOnboardingCancel = useCallback(() => { + const transaction = communityOnboarding.transaction; + communityOnboarding.clear(); + + if (!transaction?.communityId) return; + if (!transaction.addedCommunity) { + if (transaction.previousCommunityId) { + switchCommunity(transaction.previousCommunityId); + } + return; + } + if (communities.length === 1) { + if (transaction.source === "first-community") { + setResumeFirstCommunityJoin(true); + } + clearCommunities(); + return; + } + removeCommunity(transaction.communityId); + if (transaction.previousCommunityId) { + switchCommunity(transaction.previousCommunityId); + } + }, [ + clearCommunities, + communities.length, + communityOnboarding, + removeCommunity, + switchCommunity, + ]); const bootSplashPhase = useBootSplashHold(); const transaction = communityOnboarding.transaction; + useEffect(() => { + if (transaction?.stage !== "connecting") { + connectingTransactionRef.current = null; + } + }, [transaction?.stage]); const targetIsReady = transaction?.communityId === activeCommunity?.id && community.isReady && community.appliedKey === communityKey; useEffect(() => { - if ( - transaction?.stage !== "connecting" || - transaction.error || - !targetIsReady - ) { - return; + if (transaction?.stage === "connecting" && targetIsReady) { + communityOnboarding.update({ stage: "profile", error: undefined }); } - - let cancelled = false; - void getMyRelayMembershipLookup() - .then(({ membership, snapshotFound }) => { - if (cancelled) return; - if (snapshotFound && membership === null) { - communityOnboarding.update({ - error: - "You have not been added to this community yet. Ask the host to add your public key, then try again.", - }); - return; - } - communityOnboarding.update({ stage: "profile", error: undefined }); - }) - .catch((error: unknown) => { - if (cancelled) return; - communityOnboarding.update({ - error: isRelayMembershipDeniedError(error) - ? "You have not been added to this community yet. Ask the host to add your public key, then try again." - : "Could not check community access. Check the URL and try again.", - }); - }); - - return () => { - cancelled = true; - }; - }, [ - communityOnboarding.update, - targetIsReady, - transaction?.error, - transaction?.stage, - ]); + }, [communityOnboarding.update, targetIsReady, transaction?.stage]); // During "entering" the transaction stays alive as a curtain: the app mounts // underneath (already pointed at the Welcome channel route) while the // onboarding screen covers it, then fades once Welcome reports ready. @@ -398,6 +411,7 @@ function CommunityApp({ appContent = ( ); @@ -465,6 +479,7 @@ function CommunityApp({ } > diff --git a/desktop/src/features/communities/ui/WelcomeSetup.tsx b/desktop/src/features/communities/ui/WelcomeSetup.tsx index 7c4dbdaaec..4c84975132 100644 --- a/desktop/src/features/communities/ui/WelcomeSetup.tsx +++ b/desktop/src/features/communities/ui/WelcomeSetup.tsx @@ -32,6 +32,7 @@ type WelcomeTransitionMode = "initial" | OnboardingTransitionDirection; type WelcomeSetupProps = { defaultRelayUrl: string; + initialPage?: WelcomeSetupPage; initialTransitionMode?: WelcomeTransitionMode; onBack: () => void; }; @@ -50,10 +51,11 @@ function isLocalDevRelayUrl(relayUrl: string) { export function WelcomeSetup({ defaultRelayUrl, + initialPage = "welcome", initialTransitionMode = "initial", onBack, }: WelcomeSetupProps) { - const [page, setPage] = React.useState("welcome"); + const [page, setPage] = React.useState(initialPage); const [transitionMode, setTransitionMode] = React.useState(initialTransitionMode); const [npub, setNpub] = React.useState(""); diff --git a/desktop/src/features/onboarding/communityOnboarding.tsx b/desktop/src/features/onboarding/communityOnboarding.tsx index 5388c100a3..1f0fe45ec7 100644 --- a/desktop/src/features/onboarding/communityOnboarding.tsx +++ b/desktop/src/features/onboarding/communityOnboarding.tsx @@ -42,6 +42,8 @@ export type CommunityOnboardingTransaction = { */ policyReceipt?: string; communityId?: string; + previousCommunityId?: string; + addedCommunity?: boolean; createdAt: string; updatedAt: string; error?: string; @@ -56,6 +58,8 @@ export type CommunityOnboardingTransactionPatch = Partial< | "stage" | "relayUrl" | "communityId" + | "previousCommunityId" + | "addedCommunity" | "communityName" | "error" | "acknowledged" diff --git a/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx b/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx index e4b27fd673..f28dd8de43 100644 --- a/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx +++ b/desktop/src/features/onboarding/ui/CommunityOnboardingFlow.tsx @@ -113,8 +113,10 @@ function AvatarCircle({ } export function CommunityOnboardingFlow({ + onCancel, onConnect, }: { + onCancel: () => void; onConnect: () => void; }) { const { transaction, update, clear } = useCommunityOnboarding(); @@ -372,7 +374,7 @@ export function CommunityOnboardingFlow({ ) : null}