diff --git a/apps/desktop/src/router.tsx b/apps/desktop/src/router.tsx index b934ee9b..1aabdd5f 100644 --- a/apps/desktop/src/router.tsx +++ b/apps/desktop/src/router.tsx @@ -189,12 +189,35 @@ export default function Router() { }, }, { - path: "import", + path: "login", async lazy() { - const { ImportAccountScreen } = await import( - "./routes/auth/import" + const { LoginScreen } = await import("./routes/auth/login"); + return { Component: LoginScreen }; + }, + }, + { + path: "login-key", + async lazy() { + const { LoginWithKey } = await import("./routes/auth/login-key"); + return { Component: LoginWithKey }; + }, + }, + { + path: "login-nsecbunker", + async lazy() { + const { LoginWithNsecbunker } = await import( + "./routes/auth/login-nsecbunker" ); - return { Component: ImportAccountScreen }; + return { Component: LoginWithNsecbunker }; + }, + }, + { + path: "login-oauth", + async lazy() { + const { LoginWithOAuth } = await import( + "./routes/auth/login-oauth" + ); + return { Component: LoginWithOAuth }; }, }, { diff --git a/apps/desktop/src/routes/auth/create.tsx b/apps/desktop/src/routes/auth/create.tsx index ae3508eb..27631f31 100644 --- a/apps/desktop/src/routes/auth/create.tsx +++ b/apps/desktop/src/routes/auth/create.tsx @@ -89,74 +89,86 @@ export function CreateAccountScreen() { }; const onSubmit = async (data: { username: string; email: string }) => { - setIsLoading(true); + try { + setIsLoading(true); - const domain = getDomainName(serviceId); - const service = services.find((ev) => ev.id === serviceId); + const domain = getDomainName(serviceId); + const service = services.find((ev) => ev.id === serviceId); - const localSigner = NDKPrivateKeySigner.generate(); - const localUser = await localSigner.user(); - - const bunker = new NDK({ - explicitRelayUrls: [ - "wss://relay.nsecbunker.com/", - "wss://nostr.vulpem.com/", - ], - }); - - await bunker.connect(2000); - - const remoteSigner = new NDKNip46Signer( - bunker, - service.pubkey, - localSigner, - ); - - let authWindow: Window; - - remoteSigner.addListener("authUrl", (authUrl: string) => { - authWindow = new Window(`auth-${serviceId}`, { - url: authUrl, - title: domain, - titleBarStyle: "overlay", - width: 415, - height: 600, - center: true, - closable: false, + // generate ndk for nsecbunker + const localSigner = NDKPrivateKeySigner.generate(); + const bunker = new NDK({ + explicitRelayUrls: [ + "wss://relay.nsecbunker.com/", + "wss://nostr.vulpem.com/", + ], }); - }); + await bunker.connect(2000); - const account = await remoteSigner.createAccount( - data.username, - domain, - data.email, - ); + // generate tmp remote singer for create account + const remoteSigner = new NDKNip46Signer( + bunker, + service.pubkey, + localSigner, + ); - if (!account) { - authWindow.close(); + // handle auth url request + let authWindow: Window; + remoteSigner.addListener("authUrl", (authUrl: string) => { + authWindow = new Window(`auth-${serviceId}`, { + url: authUrl, + title: domain, + titleBarStyle: "overlay", + width: 415, + height: 600, + center: true, + closable: false, + }); + }); + + // create new account + const account = await remoteSigner.createAccount( + data.username, + domain, + data.email, + ); + + if (!account) { + authWindow.close(); + setIsLoading(false); + + return toast.error("Failed to create new account, try again later"); + } + + // add account to storage + await storage.createSetting("nsecbunker", "1"); + await storage.createAccount({ + pubkey: account, + privkey: localSigner.privateKey, + }); + + // get final signer with newly created account + const finalSigner = new NDKNip46Signer(bunker, account, localSigner); + await finalSigner.blockUntilReady(); + + // update main ndk instance signer + ark.updateNostrSigner({ signer: finalSigner }); + console.log(ark.ndk.signer); + + // remove default nsecbunker profile and contact list + await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] }); + await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] }); + + setOnboarding(true); setIsLoading(false); - return toast.error("Failed to create new account, try again later"); + authWindow.close(); + + return navigate("/auth/onboarding"); + } catch (e) { + setIsLoading(false); + toast.error(String(e)); } - - await storage.createSetting("nsecbunker", "1"); - await storage.createAccount({ - pubkey: account, - privkey: localSigner.privateKey, - }); - - ark.updateNostrSigner({ signer: remoteSigner }); - - // remove default nsecbunker profile and contact list - await ark.createEvent({ kind: NDKKind.Metadata, content: "", tags: [] }); - await ark.createEvent({ kind: NDKKind.Contacts, content: "", tags: [] }); - - setOnboarding(true); - setIsLoading(false); - - authWindow.close(); - - return navigate("/auth/onboarding"); }; return ( diff --git a/apps/desktop/src/routes/auth/follows.tsx b/apps/desktop/src/routes/auth/follows.tsx deleted file mode 100644 index 25ffb06d..00000000 --- a/apps/desktop/src/routes/auth/follows.tsx +++ /dev/null @@ -1,259 +0,0 @@ -import { useArk } from "@lume/ark"; -import { - ArrowRightIcon, - CancelIcon, - ChevronDownIcon, - LoaderIcon, - PlusIcon, -} from "@lume/icons"; -import { User } from "@lume/ui"; -import * as Accordion from "@radix-ui/react-accordion"; -import { useQuery } from "@tanstack/react-query"; -import { nip19 } from "nostr-tools"; -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { toast } from "sonner"; -import { twMerge } from "tailwind-merge"; - -const POPULAR_USERS = [ - "npub180cvv07tjdrrgpa0j7j7tmnyl2yr6yr7l8j4s3evf6u64th6gkwsyjh6w6", - "npub1sg6plzptd64u62a878hep2kev88swjh3tw00gjsfl8f237lmu63q0uf63m", - "npub1xtscya34g58tk0z605fvr788k263gsu6cy9x0mhnm87echrgufzsevkk5s", - "npub1gcxzte5zlkncx26j68ez60fzkvtkm9e0vrwdcvsjakxf9mu9qewqlfnj5z", - "npub1az9xj85cmxv8e9j9y80lvqp97crsqdu2fpu3srwthd99qfu9qsgstam8y8", - "npub1a2cww4kn9wqte4ry70vyfwqyqvpswksna27rtxd8vty6c74era8sdcw83a", - "npub168ghgug469n4r2tuyw05dmqhqv5jcwm7nxytn67afmz8qkc4a4zqsu2dlc", - "npub133vj8ycevdle0cq8mtgddq0xtn34kxkwxvak983dx0u5vhqnycyqj6tcza", - "npub18ams6ewn5aj2n3wt2qawzglx9mr4nzksxhvrdc4gzrecw7n5tvjqctp424", - "npub1r0rs5q2gk0e3dk3nlc7gnu378ec6cnlenqp8a3cjhyzu6f8k5sgs4sq9ac", - "npub1prya33fnqerq0fljwjtp77ehtu7jlsjt5ydhwveuwmqdsdm6k8esk42xcv", - "npub19mduaf5569jx9xz555jcx3v06mvktvtpu0zgk47n4lcpjsz43zzqhj6vzk", -]; - -const LUME_USERS = [ - "npub1zfss807aer0j26mwp2la0ume0jqde3823rmu97ra6sgyyg956e0s6xw445", -]; - -export function FollowsScreen() { - const ark = useArk(); - const navigate = useNavigate(); - - const { status, data } = useQuery({ - queryKey: ["trending-users"], - queryFn: async () => { - const res = await fetch("https://api.nostr.band/v0/trending/profiles"); - if (!res.ok) { - throw new Error("Failed to fetch trending users from nostr.band API."); - } - return res.json(); - }, - }); - - const [loading, setLoading] = useState(false); - const [follows, setFollows] = useState([]); - - // toggle follow state - const toggleFollow = (pubkey: string) => { - const arr = follows.includes(pubkey) - ? follows.filter((i) => i !== pubkey) - : [...follows, pubkey]; - setFollows(arr); - }; - - const submit = async () => { - try { - setLoading(true); - if (!follows.length) return navigate("/auth/finish"); - - const publish = await ark.newContactList({ - tags: follows.map((item) => { - if (item.startsWith("npub1")) - return ["p", nip19.decode(item).data as string]; - return ["p", item]; - }), - }); - - if (publish) { - setLoading(false); - return navigate("/auth/finish"); - } - } catch (e) { - setLoading(false); - toast.error(e); - } - }; - - return ( -
-
-
-

- Dive into the nostrverse -

-

- Try following some users that interest you -
- to build up your timeline. -

-
- - - - Popular users - - - -
- {POPULAR_USERS.map((pubkey) => ( -
- - -
- ))} -
-
-
- - - Trending users - - - -
- {status === "pending" ? ( -
- -
- ) : ( - data?.profiles.map( - (item: { - pubkey: string; - profile: { content: string }; - }) => ( -
- - -
- ), - ) - )} -
-
-
- - - Lume HQ - - - -
- {LUME_USERS.map((pubkey) => ( -
- - -
- ))} -
-
-
-
-
-
- -
-
- ); -} diff --git a/apps/desktop/src/routes/auth/import.tsx b/apps/desktop/src/routes/auth/import.tsx deleted file mode 100644 index ca5c23ff..00000000 --- a/apps/desktop/src/routes/auth/import.tsx +++ /dev/null @@ -1,330 +0,0 @@ -import { useArk, useStorage } from "@lume/ark"; -import { ArrowLeftIcon, LoaderIcon } from "@lume/icons"; -import { User } from "@lume/ui"; -import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; -import { readText } from "@tauri-apps/plugin-clipboard-manager"; -import { open } from "@tauri-apps/plugin-shell"; -import { motion } from "framer-motion"; -import { nip19 } from "nostr-tools"; -import { useState } from "react"; -import { useNavigate } from "react-router-dom"; -import { toast } from "sonner"; -import { twMerge } from "tailwind-merge"; - -export function ImportAccountScreen() { - const [npub, setNpub] = useState(""); - const [nsec, setNsec] = useState(""); - const [pubkey, setPubkey] = useState(undefined); - const [loading, setLoading] = useState(false); - const [created, setCreated] = useState({ ok: false, remote: false }); - const [savedPrivkey, setSavedPrivkey] = useState(false); - - const ark = useArk(); - const storage = useStorage(); - const navigate = useNavigate(); - - const submitNpub = async () => { - if (npub.length < 6) return toast.error("You must enter valid npub"); - if (!npub.startsWith("npub1")) - return toast.error("npub must be starts with npub1"); - - try { - const pubkey = nip19.decode(npub).data as string; - setPubkey(pubkey); - } catch (e) { - return toast.error(`npub invalid: ${e}`); - } - }; - - const connectNsecBunker = async () => { - if (npub.length < 6) return toast.error("You must enter valid npub"); - if (!npub.startsWith("npub1")) - return toast.error("npub must be starts with npub1"); - - try { - const pubkey = nip19.decode(npub.split("#")[0]).data as string; - const localSigner = NDKPrivateKeySigner.generate(); - - await storage.createSetting("nsecbunker", "1"); - await storage.createPrivkey(`${npub}-nsecbunker`, localSigner.privateKey); - - // open nsecbunker web app in default browser - await open("https://app.nsecbunker.com/keys"); - - const bunker = new NDK({ - explicitRelayUrls: [ - "wss://relay.nsecbunker.com", - "wss://nostr.vulpem.com", - ], - }); - await bunker.connect(); - - const remoteSigner = new NDKNip46Signer(bunker, npub, localSigner); - await remoteSigner.blockUntilReady(); - ark.updateNostrSigner({ signer: remoteSigner }); - - setPubkey(pubkey); - setCreated({ ok: false, remote: true }); - } catch (e) { - return toast.error(e); - } - }; - - const changeAccount = async () => { - setNpub(""); - setPubkey(""); - }; - - const createAccount = async () => { - try { - setLoading(true); - - // add account to db - await storage.createAccount({ id: npub, pubkey }); - - // get account contacts - await ark.getUserContacts({ pubkey }); - - setCreated((prev) => ({ ...prev, ok: true })); - setLoading(false); - - if (created.remote) navigate("/auth/onboarding"); - } catch (e) { - setLoading(false); - return toast.error(e); - } - }; - - const pasteNsec = async () => { - const tempNsec = await readText(); - setNsec(tempNsec); - }; - - const submitNsec = async () => { - if (savedPrivkey) return; - if (nsec.length > 50 && nsec.startsWith("nsec1")) { - try { - const privkey = nip19.decode(nsec).data as string; - await storage.createPrivkey(pubkey, privkey); - ark.updateNostrSigner({ signer: new NDKPrivateKeySigner(privkey) }); - - setSavedPrivkey(true); - } catch (e) { - return toast(`nsec invalid: ${e}`); - } - } - }; - - return ( -
-
- {!created ? ( - - ) : null} -
-
-

- Import your account. -

-
-
-
- -
- setNpub(e.target.value)} - spellCheck={false} - autoComplete="off" - autoCorrect="off" - autoCapitalize="off" - placeholder="npub1" - className="h-11 rounded-lg border-transparent bg-neutral-100 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-900 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" - /> - {!pubkey ? ( -
- - -
- ) : null} - {npub.indexOf("#") > -1 ? ( -

- You're using nsecbunker token, keep in mind it only can - redeem one-time, you need to login again in the next launch -

- ) : null} -
-
-
- {pubkey ? ( - -
Account found
-
-
- - -
- {!created.ok ? ( - - ) : null} -
-
- ) : null} - {created.ok ? ( - <> - {!created.remote ? ( - -
- -
-
- setNsec(e.target.value)} - spellCheck={false} - autoComplete="off" - autoCorrect="off" - autoCapitalize="off" - placeholder="nsec1" - className="h-11 w-full rounded-lg border-transparent bg-neutral-200 px-3 placeholder:text-neutral-500 focus:border-blue-500 focus:ring focus:ring-blue-200 dark:bg-neutral-800 dark:placeholder:text-neutral-400 dark:focus:ring-blue-800" - /> - {nsec.length < 5 ? ( -
- -
- ) : null} -
- {nsec.length > 5 ? ( - - ) : null} -
-
-
-

- Private Key is used to sign your event. For - example, if you want to make a new post or send a message - to your contact, you need to use your private key to sign - this event. -

-
- 1. In case you store private key in Lume -
-

- Lume will put your private key to{" "} - - {storage.platform === "macos" - ? "Apple Keychain (macOS)" - : storage.platform === "windows" - ? "Credential Manager (Windows)" - : "Secret Service (Linux)"} - - , it will be secured by your OS -

-
- 2. In case you do not store private key in Lume -
-

- When you make an event that requires a sign by your - private key, Lume will show a prompt for you to enter - private key. It will be cleared after signing and not - stored anywhere. -

-
-
- ) : null} - navigate("/auth/onboarding")} - > - Continue - - - ) : null} -
-
-
- ); -} diff --git a/apps/desktop/src/routes/auth/login-key.tsx b/apps/desktop/src/routes/auth/login-key.tsx new file mode 100644 index 00000000..22f455c5 --- /dev/null +++ b/apps/desktop/src/routes/auth/login-key.tsx @@ -0,0 +1,115 @@ +import { useStorage } from "@lume/ark"; +import { EyeOffIcon, EyeOnIcon, LoaderIcon } from "@lume/icons"; +import { getPublicKey, nip19 } from "nostr-tools"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; + +export function LoginWithKey() { + const storage = useStorage(); + const navigate = useNavigate(); + + const [showKey, setShowKey] = useState(false); + const [loading, setLoading] = useState(false); + + const { + register, + handleSubmit, + setError, + formState: { errors, isValid }, + } = useForm(); + + const onSubmit = async (data: { nsec: string }) => { + try { + if (!data.nsec.startsWith("nsec1")) + return toast.error("You need to enter a private key start with nsec1"); + + setLoading(true); + + const privkey = nip19.decode(data.nsec).data as string; + const pubkey = getPublicKey(privkey); + + await storage.createAccount({ + pubkey: pubkey, + privkey: privkey, + }); + + return navigate("/auth/onboarding"); + } catch (e) { + setLoading(false); + setError("nsec", { + type: "manual", + message: String(e), + }); + } + }; + + return ( +
+
+
+

Enter your Private Key

+

+ Lume will put your private key to{" "} + + {storage.platform === "macos" + ? "Apple Keychain" + : storage.platform === "windows" + ? "Credential Manager" + : "Secret Service"} + + . +
+ It will be secured by your OS. +

+
+
+
+
+ + {errors.nsec && ( +

+ {errors.nsec.message as string} +

+ )} + +
+ +
+
+
+
+ ); +} diff --git a/apps/desktop/src/routes/auth/login-nsecbunker.tsx b/apps/desktop/src/routes/auth/login-nsecbunker.tsx new file mode 100644 index 00000000..0616fa68 --- /dev/null +++ b/apps/desktop/src/routes/auth/login-nsecbunker.tsx @@ -0,0 +1,110 @@ +import { useArk, useStorage } from "@lume/ark"; +import { LoaderIcon } from "@lume/icons"; +import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; +import { nip19 } from "nostr-tools"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; + +export function LoginWithNsecbunker() { + const ark = useArk(); + const storage = useStorage(); + const navigate = useNavigate(); + + const [loading, setLoading] = useState(false); + + const { + register, + handleSubmit, + setError, + formState: { errors, isValid }, + } = useForm(); + + const onSubmit = async (data: { npub: string }) => { + try { + if (!data.npub.startsWith("npub1")) + return toast.info("You need to enter a token start with npub1"); + + if (!data.npub.includes("#")) + return toast.info("Token must include #secret"); + + setLoading(true); + + const bunker = new NDK({ + explicitRelayUrls: [ + "wss://relay.nsecbunker.com", + "wss://nostr.vulpem.com", + ], + }); + await bunker.connect(2000); + + const pubkey = nip19.decode(data.npub.split("#")[0]).data as string; + const localSigner = NDKPrivateKeySigner.generate(); + const remoteSigner = new NDKNip46Signer(bunker, data.npub, localSigner); + await remoteSigner.blockUntilReady(); + + ark.updateNostrSigner({ signer: remoteSigner }); + + await storage.createSetting("nsecbunker", "1"); + await storage.createAccount({ + pubkey, + privkey: localSigner.privateKey, + }); + + return navigate("/auth/onboarding"); + } catch (e) { + setLoading(false); + setError("npub", { + type: "manual", + message: String(e), + }); + } + }; + + return ( +
+
+
+

+ Enter your nsecbunker token +

+
+
+
+
+ + {errors.npub && ( +

+ {errors.npub.message as string} +

+ )} +
+ +
+
+
+
+ ); +} diff --git a/apps/desktop/src/routes/auth/login-oauth.tsx b/apps/desktop/src/routes/auth/login-oauth.tsx new file mode 100644 index 00000000..b4ee93db --- /dev/null +++ b/apps/desktop/src/routes/auth/login-oauth.tsx @@ -0,0 +1,139 @@ +import { useArk, useStorage } from "@lume/ark"; +import { LoaderIcon } from "@lume/icons"; +import { NIP05 } from "@lume/types"; +import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from "@nostr-dev-kit/ndk"; +import { Window } from "@tauri-apps/api/window"; +import { fetch } from "@tauri-apps/plugin-http"; +import { nip19 } from "nostr-tools"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { useNavigate } from "react-router-dom"; +import { toast } from "sonner"; + +const emailRegex = /^[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,4}$/; + +export function LoginWithOAuth() { + const ark = useArk(); + const storage = useStorage(); + const navigate = useNavigate(); + + const [loading, setLoading] = useState(false); + + const { + register, + handleSubmit, + setError, + formState: { errors, isValid }, + } = useForm(); + + const onSubmit = async (data: { nip05: string }) => { + try { + setLoading(true); + + const localPath = data.nip05.split("@")[0]; + const service = data.nip05.split("@")[1]; + + const verifyURL = `https://${service}/.well-known/nostr.json?name=${localPath}`; + + const req = await fetch(verifyURL, { + method: "GET", + headers: { + "Content-Type": "application/json; charset=utf-8", + }, + }); + + if (!req.ok) + return toast.error( + "Cannot verify your NIP-05 address, please try again later.", + ); + + const res: NIP05 = await req.json(); + + if (!res.names[localPath.toLowerCase()] || !res.names[localPath]) + return toast.error( + "Cannot verify your NIP-05 address, please try again later.", + ); + + const pubkey = + (res.names[localPath] as string) || + (res.names[localPath.toLowerCase()] as string); + + if (!res.nip46[pubkey]) + return toast.error("Cannot found NIP-46 with this address"); + + const nip46Relays = res.nip46[pubkey] as unknown as string[]; + + const bunker = new NDK({ + explicitRelayUrls: nip46Relays || [ + "wss://relay.nsecbunker.com", + "wss://nostr.vulpem.com", + ], + }); + await bunker.connect(2000); + + const localSigner = NDKPrivateKeySigner.generate(); + const remoteSigner = new NDKNip46Signer(bunker, pubkey, localSigner); + await remoteSigner.blockUntilReady(); + + ark.updateNostrSigner({ signer: remoteSigner }); + + await storage.createSetting("nsecbunker", "1"); + await storage.createAccount({ + pubkey, + privkey: localSigner.privateKey, + }); + + return navigate("/auth/onboarding"); + } catch (e) { + setLoading(false); + setError("npub", { + type: "manual", + message: String(e), + }); + } + }; + + return ( +
+
+
+

Enter your NIP-05 address

+
+
+
+
+ + {errors.nip05 && ( +

+ {errors.nip05.message as string} +

+ )} +
+ +
+
+
+
+ ); +} diff --git a/apps/desktop/src/routes/auth/login.tsx b/apps/desktop/src/routes/auth/login.tsx new file mode 100644 index 00000000..a6cb7d20 --- /dev/null +++ b/apps/desktop/src/routes/auth/login.tsx @@ -0,0 +1,55 @@ +import { Link } from "react-router-dom"; + +export function LoginScreen() { + return ( +
+
+
+

+ Continue your experience on Nostr +

+
+
+
+ + OAuth Login + + + Login with nsecbunker + +
+
+
+
+
+
+
+ + Or (Not recommend) + +
+
+
+ + Login with Private Key + +

+ Lume will store your Private Key in{" "} + OS Secure Storage +

+
+
+
+
+
+ ); +} diff --git a/apps/desktop/src/routes/auth/onboarding.tsx b/apps/desktop/src/routes/auth/onboarding.tsx index 27cde070..21dc7f88 100644 --- a/apps/desktop/src/routes/auth/onboarding.tsx +++ b/apps/desktop/src/routes/auth/onboarding.tsx @@ -1,7 +1,9 @@ -import { useStorage } from "@lume/ark"; +import { useArk, useStorage } from "@lume/ark"; import { InfoIcon, LoaderIcon } from "@lume/icons"; -import { delay } from "@lume/utils"; +import { FETCH_LIMIT } from "@lume/utils"; +import { NDKKind } from "@nostr-dev-kit/ndk"; import * as Switch from "@radix-ui/react-switch"; +import { useQueryClient } from "@tanstack/react-query"; import { isPermissionGranted, requestPermission, @@ -10,7 +12,9 @@ import { useEffect, useState } from "react"; import { useNavigate } from "react-router-dom"; export function OnboardingScreen() { + const ark = useArk(); const storage = useStorage(); + const queryClient = useQueryClient(); const navigate = useNavigate(); const [loading, setLoading] = useState(false); @@ -33,7 +37,35 @@ export function OnboardingScreen() { const completeAuth = async () => { setLoading(true); - await delay(1200); + + // get account contacts + await ark.getUserContacts(storage.account.pubkey); + + // refetch newsfeed + await queryClient.prefetchInfiniteQuery({ + queryKey: ["timeline-9999"], + initialPageParam: 0, + queryFn: async ({ + signal, + pageParam, + }: { + signal: AbortSignal; + pageParam: number; + }) => { + const events = await ark.getInfiniteEvents({ + filter: { + kinds: [NDKKind.Text, NDKKind.Repost], + authors: storage.account.contacts, + }, + limit: FETCH_LIMIT, + pageParam, + signal, + }); + + return events; + }, + }); + navigate("/"); }; diff --git a/apps/desktop/src/routes/auth/welcome.tsx b/apps/desktop/src/routes/auth/welcome.tsx index f10028cb..6b4095cb 100644 --- a/apps/desktop/src/routes/auth/welcome.tsx +++ b/apps/desktop/src/routes/auth/welcome.tsx @@ -41,7 +41,7 @@ export function WelcomeScreen() { )} Login diff --git a/apps/desktop/src/routes/settings/components/contactCard.tsx b/apps/desktop/src/routes/settings/components/contactCard.tsx index c293af4d..9af13390 100644 --- a/apps/desktop/src/routes/settings/components/contactCard.tsx +++ b/apps/desktop/src/routes/settings/components/contactCard.tsx @@ -9,7 +9,7 @@ export function ContactCard() { const { status, data } = useQuery({ queryKey: ["contacts"], queryFn: async () => { - const contacts = await ark.getUserContacts({}); + const contacts = await ark.getUserContacts(); return contacts; }, refetchOnWindowFocus: false, diff --git a/apps/desktop/src/routes/settings/editContact.tsx b/apps/desktop/src/routes/settings/editContact.tsx index f9691c79..791e41bd 100644 --- a/apps/desktop/src/routes/settings/editContact.tsx +++ b/apps/desktop/src/routes/settings/editContact.tsx @@ -8,7 +8,7 @@ export function EditContactScreen() { const { status, data } = useQuery({ queryKey: ["contacts"], queryFn: async () => { - return await ark.getUserContacts({}); + return await ark.getUserContacts(); }, refetchOnWindowFocus: false, }); diff --git a/packages/ark/src/ark.ts b/packages/ark/src/ark.ts index 8ffe66ad..0764ddfb 100644 --- a/packages/ark/src/ark.ts +++ b/packages/ark/src/ark.ts @@ -108,10 +108,11 @@ export class Ark { } } - public async getUserProfile({ pubkey }: { pubkey: string }) { + public async getUserProfile(pubkey?: string) { try { // get clean pubkey without any special characters let hexstring = pubkey.replace(/[^a-zA-Z0-9]/g, "").replace("nostr:", ""); + const currentUserPubkey = this.#storage.account.pubkey; if ( hexstring.startsWith("npub1") || @@ -125,7 +126,9 @@ export class Ark { if (decoded.type === "naddr") hexstring = decoded.data.pubkey; } - const user = this.ndk.getUser({ pubkey: hexstring }); + const user = this.ndk.getUser({ + pubkey: pubkey ? hexstring : currentUserPubkey, + }); const profile = await user.fetchProfile({ cacheUsage: NDKSubscriptionCacheUsage.CACHE_FIRST, @@ -138,23 +141,33 @@ export class Ark { } } - public async getUserContacts({ - pubkey = undefined, - outbox = undefined, - }: { - pubkey?: string; - outbox?: boolean; - }) { + public async getUserContacts(pubkey?: string) { try { - const user = this.ndk.getUser({ - pubkey: pubkey ? pubkey : this.#storage.account.pubkey, - }); - const contacts = [...(await user.follows(undefined, outbox))].map( - (user) => user.pubkey, - ); + // get clean pubkey without any special characters + let hexstring = pubkey.replace(/[^a-zA-Z0-9]/g, "").replace("nostr:", ""); + const currentUserPubkey = this.#storage.account.pubkey; - if (pubkey === this.#storage.account.pubkey) + if ( + hexstring.startsWith("npub1") || + hexstring.startsWith("nprofile1") || + hexstring.startsWith("naddr1") + ) { + const decoded = nip19.decode(hexstring); + + if (decoded.type === "nprofile") hexstring = decoded.data.pubkey; + if (decoded.type === "npub") hexstring = decoded.data; + if (decoded.type === "naddr") hexstring = decoded.data.pubkey; + } + + const user = this.ndk.getUser({ + pubkey: pubkey ? hexstring : currentUserPubkey, + }); + + const contacts = [...(await user.follows())].map((user) => user.pubkey); + + if (!pubkey || pubkey === this.#storage.account.pubkey) this.#storage.account.contacts = contacts; + return contacts; } catch (e) { throw new Error(e); @@ -507,7 +520,10 @@ export class Ark { signal, }); - if (!res.ok) throw new Error(`Failed to fetch NIP-05 service: ${nip05}`); + if (!res.ok) { + console.log(res); + throw new Error(`Failed to fetch NIP-05 service: ${nip05}`); + } const data: NIP05 = await res.json(); diff --git a/packages/ark/src/hooks/useProfile.ts b/packages/ark/src/hooks/useProfile.ts index 36fd8e8d..3f79d1dd 100644 --- a/packages/ark/src/hooks/useProfile.ts +++ b/packages/ark/src/hooks/useProfile.ts @@ -1,27 +1,27 @@ -import { useQuery } from '@tanstack/react-query'; -import { useArk } from '../provider'; +import { useQuery } from "@tanstack/react-query"; +import { useArk } from "../provider"; export function useProfile(pubkey: string) { - const ark = useArk(); - const { - isLoading, - isError, - data: user, - } = useQuery({ - queryKey: ['user', pubkey], - queryFn: async () => { - const profile = await ark.getUserProfile({ pubkey }); - if (!profile) - throw new Error( - `Cannot get metadata for ${pubkey}, will be retry after 10 seconds` - ); - return profile; - }, - refetchOnMount: false, - refetchOnWindowFocus: false, - refetchOnReconnect: false, - retry: 2, - }); + const ark = useArk(); + const { + isLoading, + isError, + data: user, + } = useQuery({ + queryKey: ["user", pubkey], + queryFn: async () => { + const profile = await ark.getUserProfile(pubkey); + if (!profile) + throw new Error( + `Cannot get metadata for ${pubkey}, will be retry after 10 seconds`, + ); + return profile; + }, + refetchOnMount: false, + refetchOnWindowFocus: false, + refetchOnReconnect: false, + retry: 2, + }); - return { isLoading, isError, user }; + return { isLoading, isError, user }; } diff --git a/packages/storage/index.ts b/packages/storage/index.ts index 12f5ee5a..5d41f66e 100644 --- a/packages/storage/index.ts +++ b/packages/storage/index.ts @@ -366,6 +366,7 @@ export class LumeStorage { const currentSetting = await this.checkSettingValue(key); if (!currentSetting) { + this.settings[key] === !!parseInt(value); return await this.#db.execute( "INSERT OR IGNORE INTO settings (key, value) VALUES ($1, $2);", [key, value], diff --git a/packages/types/index.d.ts b/packages/types/index.d.ts index 89c28806..3f31c565 100644 --- a/packages/types/index.d.ts +++ b/packages/types/index.d.ts @@ -159,4 +159,9 @@ export interface NIP05 { names: { [key: string]: string; }; + nip46: { + [key: string]: { + [key: string]: string[]; + }; + }; } diff --git a/packages/ui/src/onboarding/profileSettings.tsx b/packages/ui/src/onboarding/profileSettings.tsx index 8780a1d0..968afaa1 100644 --- a/packages/ui/src/onboarding/profileSettings.tsx +++ b/packages/ui/src/onboarding/profileSettings.tsx @@ -32,9 +32,7 @@ export function OnboardingProfileSettingsScreen() { navigate("/follow"); } - const oldProfile = await ark.getUserProfile({ - pubkey: storage.account.pubkey, - }); + const oldProfile = await ark.getUserProfile(); const profile: NDKUserProfile = { ...data, @@ -55,9 +53,6 @@ export function OnboardingProfileSettingsScreen() { if (publish) { setLoading(false); navigate("/follow"); - } else { - toast.error("Cannot publish your profile, please try again later."); - setLoading(false); } } catch (e) { setLoading(false);