NostrUserInput.tsx (4028B)
1 import { FormattedMessage, useIntl } from "react-intl"; 2 import Link from "next/link"; 3 import Image from "next/image"; 4 import { useEffect, useRef, useState } from "react"; 5 import { NostrEvent, Relay, nip19 } from "nostr-tools" 6 import { Loader2, Radius, Shell } from "lucide-react"; 7 import { Input } from "@/components/ui/Input" 8 import { Label } from "@/components/ui/Label" 9 import { AccountInfo, Profile, getProfile, getPurpleAccountInfo } from "@/utils/PurpleUtils"; 10 import { ErrorDialog } from "./ErrorDialog"; 11 import { NostrProfile } from "./NostrProfile"; 12 13 14 // TODO: Double-check this regex and make it more accurate 15 const NPUB_REGEX = /^npub[0-9A-Za-z]+$/ 16 17 export function NostrUserInput(props: { pubkey: string | null, setPubkey: (pubkey: string | null) => void, onProfileChange: (profile: Profile | undefined | null) => void, disabled?: boolean | undefined, profileHeader?: React.ReactNode, profileFooter?: React.ReactNode }) { 18 const intl = useIntl() 19 const [profile, setProfile] = useState<Profile | undefined | null>(undefined) // The profile info fetched from the Damus relay 20 const [error, setError] = useState<string | null>(null) // An error message to display to the user 21 const [npubValidationError, setNpubValidationError] = useState<string | null>(null) 22 const [npub, setNpub] = useState<string>("") 23 24 // MARK: - Functions 25 26 const fetchProfile = async () => { 27 if (!props.pubkey) { 28 return 29 } 30 try { 31 const profile = await getProfile(props.pubkey) 32 setProfile(profile) 33 } 34 catch (e) { 35 console.error(e) 36 setError("Failed to get profile info from the relay. Please wait a few minutes and refresh the page. If the problem persists, please contact support.") 37 } 38 } 39 40 // MARK: - Effects and hooks 41 42 // Load the profile when the pubkey changes 43 useEffect(() => { 44 if (props.pubkey) { 45 fetchProfile() 46 } 47 }, [props.pubkey]) 48 49 useEffect(() => { 50 if (npub.length > 0 && !NPUB_REGEX.test(npub)) { 51 setNpubValidationError(intl.formatMessage({ id: "purple.login.npub-validation-error", defaultMessage: "Please enter a valid npub" })) 52 setProfile(undefined) 53 } 54 else { 55 setNpubValidationError(null) 56 if (npub.length > 0) { 57 try { 58 const decoded = nip19.decode(npub) 59 props.setPubkey(decoded.data as string) 60 } 61 catch (e) { 62 props.setPubkey(null) 63 setNpubValidationError(intl.formatMessage({ id: "purple.login.npub-validation-error", defaultMessage: "Please enter a valid npub" })) 64 } 65 } 66 else { 67 setProfile(undefined) 68 props.setPubkey(null) 69 } 70 } 71 }, [npub]) 72 73 // MARK: - Render 74 75 return (<> 76 <ErrorDialog error={error} setError={setError} /> 77 <Label htmlFor="npub" className="text-purple-200/70 font-normal"> 78 {intl.formatMessage({ id: "purple.login.npub-label", defaultMessage: "Please enter your public key (npub) below" })} 79 </Label> 80 <Input id="npub" placeholder={intl.formatMessage({ id: "purple.login.npub-placeholder", defaultMessage: "npub…" })} type="text" className="mt-2" value={npub} onChange={(e) => setNpub(e.target.value)} required disabled={props.disabled} /> 81 {npubValidationError && 82 <Label htmlFor="npub" className="text-red-500 font-normal"> 83 {npubValidationError} 84 </Label> 85 } 86 {(profile === undefined && props.pubkey && props.pubkey.length > 0) && ( 87 <div className="mt-2 flex items-center justify-center"> 88 <Loader2 className="mr-2 animate-spin text-purple-200/90" size={16} /> 89 <div className="text-purple-200/70 font-normal text-sm"> 90 {intl.formatMessage({ id: "purple.login.fetching-profile", defaultMessage: "Fetching profile info…" })} 91 </div> 92 </div> 93 )} 94 {((profile || profile === null) && props.pubkey) && (<> 95 <NostrProfile 96 pubkey={props.pubkey} 97 profile={profile} 98 profileHeader={props.profileHeader} 99 profileFooter={props.profileFooter} 100 /> 101 </>)} 102 </>) 103 }