diff --git a/src/app/auth/create.tsx b/src/app/auth/create.tsx index 6f471ddf..c360e5ca 100644 --- a/src/app/auth/create.tsx +++ b/src/app/auth/create.tsx @@ -132,7 +132,7 @@ export function CreateAccountScreen() {

- Let's set up your Nostr account. + Let's set up your account.

{!keys ? ( diff --git a/src/app/auth/import.tsx b/src/app/auth/import.tsx index 7b86be3f..85dc373f 100644 --- a/src/app/auth/import.tsx +++ b/src/app/auth/import.tsx @@ -1,3 +1,4 @@ +import { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; import { readText } from '@tauri-apps/plugin-clipboard-manager'; import { motion } from 'framer-motion'; import { nip19 } from 'nostr-tools'; @@ -6,30 +7,52 @@ import { useNavigate } from 'react-router-dom'; import { toast } from 'sonner'; import { twMerge } from 'tailwind-merge'; +import { useNDK } from '@libs/ndk/provider'; import { useStorage } from '@libs/storage/provider'; import { ArrowLeftIcon } from '@shared/icons'; import { User } from '@shared/user'; export function ImportAccountScreen() { - const { db } = useStorage(); const navigate = useNavigate(); + const { db } = useStorage(); + const { ndk } = useNDK(); + const [npub, setNpub] = useState(''); const [nsec, setNsec] = useState(''); const [pubkey, setPubkey] = useState(undefined); - const [created, setCreated] = useState(false); + const [created, setCreated] = useState({ ok: false, remote: false }); const [savedPrivkey, setSavedPrivkey] = useState(false); const submitNpub = async () => { - if (npub.length < 6) return toast('You must enter valid npub'); - if (!npub.startsWith('npub1')) return toast('npub must be starts with npub1'); + 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(`npub invalid: ${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 db.secureSave(pubkey + '-bunker', localSigner.privateKey); + + const remoteSigner = new NDKNip46Signer(ndk, npub, localSigner); + ndk.signer = remoteSigner; + + setPubkey(pubkey); + setCreated({ ok: false, remote: true }); + } catch (e) { + return toast.error(e); } }; @@ -41,7 +64,9 @@ export function ImportAccountScreen() { const createAccount = async () => { try { await db.createAccount(npub, pubkey); - setCreated(true); + setCreated((prev) => ({ ...prev, ok: true })); + + if (created.remote) navigate('/auth/onboarding', { state: { newuser: false } }); } catch (e) { return toast(`Create account failed: ${e}`); } @@ -82,7 +107,7 @@ export function ImportAccountScreen() {

- Import your Nostr account. + Import your account.

@@ -90,7 +115,7 @@ export function ImportAccountScreen() { -
+
{!pubkey ? ( - +
+ + +
) : null}
@@ -126,116 +160,116 @@ export function ImportAccountScreen() { >
Account found
-
+
+
- {!created ? ( -
- - -
+ {!created.ok ? ( + ) : null}
) : null} - {created ? ( + {created.ok ? ( <> - -
- -
-
- setNsec(e.target.value)} - spellCheck={false} - autoComplete="off" - autoCorrect="off" - autoCapitalize="off" - placeholder="nsec1" - className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400" - /> - {nsec.length < 5 ? ( -
- -
+ {!created.remote ? ( + +
+ +
+
+ setNsec(e.target.value)} + spellCheck={false} + autoComplete="off" + autoCorrect="off" + autoCapitalize="off" + placeholder="nsec1" + className="h-11 w-full rounded-lg bg-neutral-200 px-3 placeholder:text-neutral-500 dark:bg-neutral-800 dark:placeholder:text-neutral-400" + /> + {nsec.length < 5 ? ( +
+ +
+ ) : null} +
+ {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{' '} - - {db.platform === 'macos' - ? 'Apple Keychain (macOS)' - : db.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. -

-
- +
+

+ 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{' '} + + {db.platform === 'macos' + ? 'Apple Keychain (macOS)' + : db.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} - Finish + Continue ) : null} diff --git a/src/app/auth/welcome.tsx b/src/app/auth/welcome.tsx index 6e4b1d46..a0d192ff 100644 --- a/src/app/auth/welcome.tsx +++ b/src/app/auth/welcome.tsx @@ -15,13 +15,13 @@ export function WelcomeScreen() { to="/auth/create" className="inline-flex h-10 w-full items-center justify-center rounded-lg bg-blue-500 font-medium text-white hover:bg-blue-600" > - Create new Nostr account + Create new account - Log in with key + Log in
diff --git a/src/libs/ndk/instance.ts b/src/libs/ndk/instance.ts index b4412361..dc4f24ab 100644 --- a/src/libs/ndk/instance.ts +++ b/src/libs/ndk/instance.ts @@ -1,4 +1,4 @@ -import NDK from '@nostr-dev-kit/ndk'; +import NDK, { NDKNip46Signer, NDKPrivateKeySigner } from '@nostr-dev-kit/ndk'; import NDKCacheAdapterDexie from '@nostr-dev-kit/ndk-cache-dexie'; import { ndkAdapter } from '@nostr-fetch/adapter-ndk'; import { message } from '@tauri-apps/plugin-dialog'; @@ -52,9 +52,30 @@ export const NDKInstance = () => { } } + async function getSigner(instance: NDK) { + if (!db.account) return null; + + // NIP-46 Signer + const localSignerPrivkey = await db.secureLoad(db.account.pubkey + '-bunker'); + if (localSignerPrivkey) { + const localSigner = new NDKPrivateKeySigner(localSignerPrivkey); + const remoteSigner = new NDKNip46Signer(instance, db.account.id, localSigner); + // await remoteSigner.blockUntilReady(); + + return remoteSigner; + } + + // Privkey Signer + const userPrivkey = await db.secureLoad(db.account.pubkey); + if (userPrivkey) return new NDKPrivateKeySigner(userPrivkey); + + return null; + } + async function initNDK() { - const explicitRelayUrls = await getExplicitRelays(); const outboxSetting = await db.getSettingValue('outbox'); + const explicitRelayUrls = await getExplicitRelays(); + const dexieAdapter = new NDKCacheAdapterDexie({ dbName: 'lume_ndkcache' }); const instance = new NDK({ explicitRelayUrls, @@ -64,7 +85,11 @@ export const NDKInstance = () => { }); try { - await instance.connect(); + // connect + await instance.connect(2000); + // add signer + const signer = await getSigner(instance); + instance.signer = signer; } catch (error) { await message(`NDK instance init failed: ${error}`, { title: 'Lume', diff --git a/src/shared/composer/composer.tsx b/src/shared/composer/composer.tsx index 66284694..69593887 100644 --- a/src/shared/composer/composer.tsx +++ b/src/shared/composer/composer.tsx @@ -1,3 +1,4 @@ +import { NDKEvent, NDKKind } from '@nostr-dev-kit/ndk'; import { message } from '@tauri-apps/plugin-dialog'; import Image from '@tiptap/extension-image'; import Placeholder from '@tiptap/extension-placeholder'; @@ -7,20 +8,19 @@ import { convert } from 'html-to-text'; import { useState } from 'react'; import { twMerge } from 'tailwind-merge'; +import { useNDK } from '@libs/ndk/provider'; + import { MediaUploader, MentionPopup } from '@shared/composer'; import { CancelIcon, LoaderIcon } from '@shared/icons'; import { MentionNote } from '@shared/notes'; import { useComposer } from '@stores/composer'; -import { useNostr } from '@utils/hooks/useNostr'; -import { sendNativeNotification } from '@utils/notification'; - export function Composer() { const [loading, setLoading] = useState(false); const [reply, clearReply] = useComposer((state) => [state.reply, state.clearReply]); - const { publish } = useNostr(); + const { ndk } = useNDK(); const expand = useComposer((state) => state.expand); const editor = useEditor({ @@ -92,18 +92,21 @@ export function Composer() { }); // publish message - await publish({ content: serializedContent, kind: 1, tags }); + const event = new NDKEvent(ndk); + event.content = serializedContent; + event.kind = NDKKind.Text; + event.tags = tags; - // send native notifiation - await sendNativeNotification('Post has been published successfully.'); - - // update state - setLoading(false); - // reset editor - editor.commands.clearContent(); - // reset reply - if (reply.id) { - clearReply(); + const publish = event.publish(); + if (publish) { + // update state + setLoading(false); + // reset editor + editor.commands.clearContent(); + // reset reply + if (reply.id) { + clearReply(); + } } } catch { setLoading(false); diff --git a/src/utils/hooks/useNostr.ts b/src/utils/hooks/useNostr.ts index 7c40e331..c6bd222a 100644 --- a/src/utils/hooks/useNostr.ts +++ b/src/utils/hooks/useNostr.ts @@ -304,20 +304,14 @@ export function useNostr() { kind: NDKKind | number; tags: string[][]; }): Promise => { - const privkey: string = await db.secureLoad(db.account.pubkey); - // #TODO: show prompt - if (!privkey) return; - const event = new NDKEvent(ndk); - const signer = new NDKPrivateKeySigner(privkey); - event.content = content; event.kind = kind; event.created_at = Math.floor(Date.now() / 1000); event.pubkey = db.account.pubkey; event.tags = tags; - await event.sign(signer); + await event.sign(); await event.publish(); return event; diff --git a/src/utils/types.d.ts b/src/utils/types.d.ts index 06d1617b..9c42afa2 100644 --- a/src/utils/types.d.ts +++ b/src/utils/types.d.ts @@ -22,8 +22,7 @@ export interface DBEvent { } export interface Account extends NDKUserProfile { - id: number; - npub: string; + id: string; pubkey: string; follows: null | string[]; circles: null | string[];