From b01e7e8fe5ee4028bb3fb0acbf4c477a24227f41 Mon Sep 17 00:00:00 2001 From: lemon Date: Fri, 15 May 2026 21:17:53 -0700 Subject: [PATCH] Add country follows to onboarding --- src/components/InitialSyncGate.tsx | 228 ++++++++++++++++++++++++++++- 1 file changed, 220 insertions(+), 8 deletions(-) diff --git a/src/components/InitialSyncGate.tsx b/src/components/InitialSyncGate.tsx index e870c256..9d950c86 100644 --- a/src/components/InitialSyncGate.tsx +++ b/src/components/InitialSyncGate.tsx @@ -7,6 +7,7 @@ import { Download, Eye, EyeOff, + Search, Loader2, UserPlus, Users, @@ -45,6 +46,8 @@ import { AgoraBoltIcon } from "@/components/icons/AgoraBoltIcon"; import { toast } from "@/hooks/useToast"; import { useUploadFile } from "@/hooks/useUploadFile"; import { VERIFIED_FOLLOW_PACK } from "@/lib/agoraDefaults"; +import { COUNTRIES } from "@/lib/countries"; +import { createCountryIdentifier, parseCountryIdentifier } from "@/lib/countryIdentifiers"; import { genUserName } from "@/lib/genUserName"; import { cn } from "@/lib/utils"; @@ -227,19 +230,26 @@ const SUGGESTED_PACKS: { kind: number; pubkey: string; identifier: string }[] = VERIFIED_FOLLOW_PACK, ]; +const COUNTRY_LIST = Object.entries(COUNTRIES) + .map(([code, { name, flag }]) => ({ code, name, flag })) + .sort((a, b) => a.name.localeCompare(b.name)); + +const SUGGESTED_COUNTRY_CODES = ["US", "BR", "VE", "NG", "IN", "JP", "DE", "ZA"]; + // Steps for signup (includes keygen + profile) vs. settings-only (existing login) type SignupStep = "keygen" | "download" | "profile"; -type SettingsStep = "follows" | "outro"; +type SettingsStep = "countries" | "follows" | "outro"; type Step = SignupStep | SettingsStep; const SIGNUP_STEPS: Step[] = [ "keygen", "download", "profile", + "countries", "follows", "outro", ]; -const SETTINGS_STEPS: Step[] = ["follows", "outro"]; +const SETTINGS_STEPS: Step[] = ["countries", "follows", "outro"]; function SetupQuestionnaire({ onComplete, @@ -348,7 +358,7 @@ function SetupQuestionnaire({ } }, [nsec, login, next, config.appName]); - // Save settings and transition to the follows step (or outro if they have follows) + // Save settings and transition to the country step before people discovery. const handleSaveAndContinue = useCallback(async () => { setIsSaving(true); @@ -456,11 +466,7 @@ function SetupQuestionnaire({ setHasFollows(userHasFollows); setIsSaving(false); - if (userHasFollows) { - goTo("outro"); - } else { - goTo("follows"); - } + goTo("countries"); }, [updateConfig, updateSettings, user, nostr, goTo]); return ( @@ -488,6 +494,17 @@ function SetupQuestionnaire({ )} {/* Settings steps */} + {step === "countries" && ( + { + if (didFollowCountries) onPreload(); + goTo(hasFollows ? "outro" : "follows"); + }} + onBack={back} + canBack={stepIndex > 0} + /> + )} + {step === "follows" && hasFollows === false && ( { @@ -809,6 +826,201 @@ function ProfileStep({ // Settings steps // --------------------------------------------------------------------------- +function CountriesStep({ + onNext, + onBack, + canBack, +}: { + onNext: (didFollowCountries: boolean) => void; + onBack: () => void; + canBack: boolean; +}) { + const { nostr } = useNostr(); + const { user } = useCurrentUser(); + const { mutateAsync: publishEvent } = useNostrPublish(); + const queryClient = useQueryClient(); + const [search, setSearch] = useState(""); + const [selectedCodes, setSelectedCodes] = useState>(new Set()); + const [isSaving, setIsSaving] = useState(false); + + const suggestedCountries = useMemo( + () => SUGGESTED_COUNTRY_CODES + .map((code) => COUNTRY_LIST.find((country) => country.code === code)) + .filter((country): country is (typeof COUNTRY_LIST)[number] => !!country), + [], + ); + + const filteredCountries = useMemo(() => { + const query = search.trim().toLowerCase(); + if (!query) return COUNTRY_LIST; + return COUNTRY_LIST.filter( + (country) => country.name.toLowerCase().includes(query) || country.code.toLowerCase().includes(query), + ); + }, [search]); + + const visibleCountries = search.trim() ? filteredCountries.slice(0, 24) : suggestedCountries; + + const toggleCountry = (code: string) => { + setSelectedCodes((current) => { + const next = new Set(current); + if (next.has(code)) { + next.delete(code); + } else { + next.add(code); + } + return next; + }); + }; + + const handleContinue = async () => { + if (!user || selectedCodes.size === 0) { + onNext(false); + return; + } + + setIsSaving(true); + try { + const identifiers = Array.from(selectedCodes).map((code) => createCountryIdentifier(code)); + const prev = await fetchFreshEvent(nostr, { + kinds: [10015], + authors: [user.pubkey], + }); + + const existingTags = prev?.tags ?? []; + const missingIdentifiers = identifiers.filter((identifier) => !existingTags.some( + ([name, value]) => name === "i" && parseCountryIdentifier(value) === parseCountryIdentifier(identifier), + )); + + const newTags = missingIdentifiers.length === 0 + ? existingTags + : [ + ...existingTags, + ...missingIdentifiers.map((identifier) => ["i", identifier]), + ]; + + if (newTags !== existingTags) { + await publishEvent({ + kind: 10015, + content: prev?.content ?? "", + tags: newTags, + prev: prev ?? undefined, + }); + queryClient.invalidateQueries({ queryKey: ["interests", user.pubkey] }); + queryClient.invalidateQueries({ queryKey: ["following-country-feed"] }); + queryClient.invalidateQueries({ queryKey: ["following-feed"] }); + } + + onNext(true); + } catch (error) { + console.error("Failed to follow countries:", error); + toast({ + title: "Could not follow countries", + description: "Please try again or skip this step for now.", + variant: "destructive", + }); + } finally { + setIsSaving(false); + } + }; + + return ( +
+
+

+ Choose your countries +

+

+ Follow countries you care about to bring local conversations and actions into your feed. +

+
+ +
+ + setSearch(e.target.value)} + className="pl-9" + /> +
+ + {visibleCountries.length > 0 ? ( +
+ {visibleCountries.map((country) => { + const isSelected = selectedCodes.has(country.code); + return ( + + ); + })} +
+ ) : ( +
+ No countries match "{search}". +
+ )} + + {search.trim() && filteredCountries.length > visibleCountries.length && ( +

+ Keep typing to narrow {filteredCountries.length} matches. +

+ )} + +
+ + +
+
+ ); +} + // --------------------------------------------------------------------------- // Follow Packs Step // ---------------------------------------------------------------------------