diff --git a/src/components/NcryptsecPasswordPrompt/index.tsx b/src/components/NcryptsecPasswordPrompt/index.tsx
new file mode 100644
index 00000000..a6b24acf
--- /dev/null
+++ b/src/components/NcryptsecPasswordPrompt/index.tsx
@@ -0,0 +1,98 @@
+import { Button } from '@/components/ui/button'
+import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog'
+import { Drawer, DrawerContent, DrawerDescription, DrawerHeader, DrawerTitle } from '@/components/ui/drawer'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
+import { useScreenSize } from '@/providers/ScreenSizeProvider'
+import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+
+type Props = {
+ open: boolean
+ onResult: (password: string | null) => void
+}
+
+export default function NcryptsecPasswordPrompt({ open, onResult }: Props) {
+ const { t } = useTranslation()
+ const { isSmallScreen } = useScreenSize()
+ const [password, setPassword] = useState('')
+
+ useEffect(() => {
+ if (open) setPassword('')
+ }, [open])
+
+ const title = t('Enter the password to decrypt your ncryptsec')
+
+ const submit = () => {
+ const trimmed = password.trim()
+ onResult(trimmed.length > 0 ? trimmed : null)
+ }
+
+ const cancel = () => {
+ onResult(null)
+ }
+
+ const form = (
+
+ )
+
+ if (isSmallScreen) {
+ return (
+ {
+ if (!next) cancel()
+ }}
+ >
+
+
+ {title}
+ {title}
+
+ {form}
+
+
+ )
+ }
+
+ return (
+
+ )
+}
diff --git a/src/components/SaveRelayDropdownMenu/index.tsx b/src/components/SaveRelayDropdownMenu/index.tsx
index 8a7fd0fa..d8403aeb 100644
--- a/src/components/SaveRelayDropdownMenu/index.tsx
+++ b/src/components/SaveRelayDropdownMenu/index.tsx
@@ -1,7 +1,15 @@
import { Button } from '@/components/ui/button'
+import {
+ Dialog,
+ DialogContent,
+ DialogDescription,
+ DialogHeader,
+ DialogTitle
+} from '@/components/ui/dialog'
import {
Drawer,
DrawerContent,
+ DrawerDescription,
DrawerHeader,
DrawerOverlay,
DrawerTitle
@@ -14,6 +22,8 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger
} from '@/components/ui/dropdown-menu'
+import { Input } from '@/components/ui/input'
+import { Label } from '@/components/ui/label'
import { Separator } from '@/components/ui/separator'
import { Skeleton } from '@/components/ui/skeleton'
import { normalizeUrl } from '@/lib/url'
@@ -22,7 +32,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { TRelaySet } from '@/types'
import { Ban, Check, FolderPlus, Plus, Star } from 'lucide-react'
-import { useMemo, useState } from 'react'
+import { useEffect, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
import DrawerMenuItem from '../DrawerMenuItem'
import logger from '@/lib/logger'
@@ -207,32 +217,133 @@ function SaveToNewSet({ urls }: { urls: string[] }) {
const { isSmallScreen } = useScreenSize()
const { pubkey, startLogin } = useNostr()
const { createRelaySet } = useFavoriteRelays()
+ const [namePromptOpen, setNamePromptOpen] = useState(false)
- const handleSave = () => {
+ const openNamePrompt = () => {
if (!pubkey) {
startLogin()
return
}
- const newSetName = prompt(t('Enter a name for the new relay set'))
- if (newSetName) {
- createRelaySet(newSetName, urls)
+ setNamePromptOpen(true)
+ }
+
+ const onNameResult = (name: string | null) => {
+ setNamePromptOpen(false)
+ if (name) {
+ createRelaySet(name, urls)
}
}
+ return (
+ <>
+ {isSmallScreen ? (
+
+
+ {t('Save to a new relay set')}
+
+ ) : (
+
+
+ {t('Save to a new relay set')}
+
+ )}
+
+ >
+ )
+}
+
+function RelaySetNamePrompt({
+ open,
+ title,
+ onResult
+}: {
+ open: boolean
+ title: string
+ onResult: (name: string | null) => void
+}) {
+ const { t } = useTranslation()
+ const { isSmallScreen } = useScreenSize()
+ const [value, setValue] = useState('')
+
+ useEffect(() => {
+ if (open) setValue('')
+ }, [open])
+
+ const submit = () => {
+ const trimmed = value.trim()
+ onResult(trimmed.length > 0 ? trimmed : null)
+ }
+
+ const cancel = () => {
+ onResult(null)
+ }
+
+ const form = (
+
+ )
+
if (isSmallScreen) {
return (
-
-
- {t('Save to a new relay set')}
-
+ {
+ if (!next) cancel()
+ }}
+ >
+
+
+ {title}
+ {title}
+
+ {form}
+
+
)
}
return (
-
-
- {t('Save to a new relay set')}
-
+
)
}
diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx
index 2d45ade7..704aa32a 100644
--- a/src/providers/NostrProvider/index.tsx
+++ b/src/providers/NostrProvider/index.tsx
@@ -1,4 +1,5 @@
import LoginDialog from '@/components/LoginDialog'
+import NcryptsecPasswordPrompt from '@/components/NcryptsecPasswordPrompt'
import {
ACCOUNT_SESSION_NETWORK_HYDRATE_MIN_INTERVAL_MS,
DEFAULT_FAVORITE_RELAYS,
@@ -95,6 +96,8 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
const [ncryptsec, setNcryptsec] = useState(null)
const [signer, setSigner] = useState(null)
const [openLoginDialog, setOpenLoginDialog] = useState(false)
+ const [ncryptsecPasswordOpen, setNcryptsecPasswordOpen] = useState(false)
+ const ncryptsecPasswordResolveRef = useRef<((value: string | null) => void) | null>(null)
const [profile, setProfile] = useState(null)
// Cleanup on page unload to prevent extension UI issues
@@ -812,6 +815,23 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
await loginWithAccountPointer(act)
}
+ const finishNcryptsecPasswordPrompt = useCallback((password: string | null) => {
+ const resolve = ncryptsecPasswordResolveRef.current
+ if (!resolve) return
+ ncryptsecPasswordResolveRef.current = null
+ setNcryptsecPasswordOpen(false)
+ resolve(password)
+ }, [])
+
+ const askNcryptsecPassword = useCallback((): Promise => {
+ return new Promise((resolve) => {
+ const prev = ncryptsecPasswordResolveRef.current
+ if (prev) prev(null)
+ ncryptsecPasswordResolveRef.current = resolve
+ setNcryptsecPasswordOpen(true)
+ })
+ }, [])
+
const nsecLogin = async (nsecOrHex: string, password?: string, needSetup?: boolean) => {
const nsecSigner = new NsecSigner()
let privkey: Uint8Array
@@ -840,11 +860,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
const ncryptsecLogin = async (ncryptsec: string) => {
- const password = prompt(t('Enter the password to decrypt your ncryptsec'))
+ const password = await askNcryptsecPassword()
if (!password) {
throw new Error('Password is required')
}
- const privkey = nip49.decrypt(ncryptsec, password)
+ let privkey: Uint8Array
+ try {
+ privkey = nip49.decrypt(ncryptsec, password)
+ } catch (e) {
+ toast.error(t('Login failed') + ': ' + (e as Error).message)
+ throw e
+ }
const browserNsecSigner = new NsecSigner()
const pubkey = browserNsecSigner.login(privkey)
return login(browserNsecSigner, { pubkey, signerType: 'ncryptsec', ncryptsec })
@@ -922,11 +948,17 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
}
} else if (account.signerType === 'ncryptsec') {
if (account.ncryptsec) {
- const password = prompt(t('Enter the password to decrypt your ncryptsec'))
+ const password = await askNcryptsecPassword()
if (!password) {
return null
}
- const privkey = nip49.decrypt(account.ncryptsec, password)
+ let privkey: Uint8Array
+ try {
+ privkey = nip49.decrypt(account.ncryptsec, password)
+ } catch (e) {
+ toast.error(t('Login failed') + ': ' + (e as Error).message)
+ return null
+ }
const browserNsecSigner = new NsecSigner()
browserNsecSigner.login(privkey)
return login(browserNsecSigner, account)
@@ -1376,6 +1408,7 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
>
{children}
+
)
}