feat: integrate nstart (#33)
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -12,6 +12,7 @@ dist
|
|||||||
dist-ssr
|
dist-ssr
|
||||||
dev-dist
|
dev-dist
|
||||||
*.local
|
*.local
|
||||||
|
.env
|
||||||
|
|
||||||
# Editor directories and files
|
# Editor directories and files
|
||||||
.vscode/*
|
.vscode/*
|
||||||
|
|||||||
6
package-lock.json
generated
6
package-lock.json
generated
@@ -35,6 +35,7 @@
|
|||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.10.4",
|
||||||
|
"nstart-modal": "^0.2.0",
|
||||||
"path-to-regexp": "^8.2.0",
|
"path-to-regexp": "^8.2.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
@@ -7011,6 +7012,11 @@
|
|||||||
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
"integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==",
|
||||||
"optional": true
|
"optional": true
|
||||||
},
|
},
|
||||||
|
"node_modules/nstart-modal": {
|
||||||
|
"version": "0.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/nstart-modal/-/nstart-modal-0.2.0.tgz",
|
||||||
|
"integrity": "sha512-rfgsSGjakAUud3Csy8xWQqjFPATvXzUfebJM4kpWbc4ljABqW0STKBYnwr7TJ5bXftWZi/X9TnSug3nziX+vhw=="
|
||||||
|
},
|
||||||
"node_modules/object-assign": {
|
"node_modules/object-assign": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
|
||||||
|
|||||||
@@ -45,6 +45,7 @@
|
|||||||
"lru-cache": "^11.0.2",
|
"lru-cache": "^11.0.2",
|
||||||
"lucide-react": "^0.469.0",
|
"lucide-react": "^0.469.0",
|
||||||
"nostr-tools": "^2.10.4",
|
"nostr-tools": "^2.10.4",
|
||||||
|
"nstart-modal": "^0.2.0",
|
||||||
"path-to-regexp": "^8.2.0",
|
"path-to-regexp": "^8.2.0",
|
||||||
"qrcode.react": "^4.2.0",
|
"qrcode.react": "^4.2.0",
|
||||||
"react": "^18.3.1",
|
"react": "^18.3.1",
|
||||||
|
|||||||
@@ -105,10 +105,19 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const onPopState = (e: PopStateEvent) => {
|
const onPopState = (e: PopStateEvent) => {
|
||||||
const state = e.state ?? { index: -1, url: '/' }
|
let state = e.state as { index: number; url: string } | null
|
||||||
setSecondaryStack((pre) => {
|
setSecondaryStack((pre) => {
|
||||||
const currentItem = pre[pre.length - 1] as TStackItem | undefined
|
const currentItem = pre[pre.length - 1] as TStackItem | undefined
|
||||||
const currentIndex = currentItem?.index
|
const currentIndex = currentItem?.index
|
||||||
|
if (!state) {
|
||||||
|
if (window.location.pathname + window.location.search + window.location.hash !== '/') {
|
||||||
|
// Just change the URL
|
||||||
|
return pre
|
||||||
|
} else {
|
||||||
|
// Back to root
|
||||||
|
state = { index: -1, url: '/' }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Go forward
|
// Go forward
|
||||||
if (currentIndex === undefined || state.index > currentIndex) {
|
if (currentIndex === undefined || state.index > currentIndex) {
|
||||||
@@ -124,7 +133,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Go back
|
// Go back
|
||||||
const newStack = pre.filter((item) => item.index <= state.index)
|
const newStack = pre.filter((item) => item.index <= state!.index)
|
||||||
const topItem = newStack[newStack.length - 1] as TStackItem | undefined
|
const topItem = newStack[newStack.length - 1] as TStackItem | undefined
|
||||||
if (!topItem) {
|
if (!topItem) {
|
||||||
// Create a new stack item if it's not exist (e.g. when the user refreshes the page, the stack will be empty)
|
// Create a new stack item if it's not exist (e.g. when the user refreshes the page, the stack will be empty)
|
||||||
|
|||||||
@@ -9,12 +9,18 @@ import { useState } from 'react'
|
|||||||
import { SimpleUserAvatar } from '../UserAvatar'
|
import { SimpleUserAvatar } from '../UserAvatar'
|
||||||
import { SimpleUsername } from '../Username'
|
import { SimpleUsername } from '../Username'
|
||||||
|
|
||||||
export default function AccountList({ afterSwitch }: { afterSwitch: () => void }) {
|
export default function AccountList({
|
||||||
|
className,
|
||||||
|
afterSwitch
|
||||||
|
}: {
|
||||||
|
className?: string
|
||||||
|
afterSwitch: () => void
|
||||||
|
}) {
|
||||||
const { accounts, account, switchAccount } = useNostr()
|
const { accounts, account, switchAccount } = useNostr()
|
||||||
const [switchingAccount, setSwitchingAccount] = useState<TAccountPointer | null>(null)
|
const [switchingAccount, setSwitchingAccount] = useState<TAccountPointer | null>(null)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-2">
|
<div className={cn('space-y-2', className)}>
|
||||||
{accounts.map((act) => (
|
{accounts.map((act) => (
|
||||||
<div
|
<div
|
||||||
key={`${act.pubkey}-${act.signerType}`}
|
key={`${act.pubkey}-${act.signerType}`}
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Separator } from '@/components/ui/separator'
|
import { Separator } from '@/components/ui/separator'
|
||||||
import { useNostr } from '@/providers/NostrProvider'
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
|
import { NstartModal } from 'nstart-modal'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import AccountList from '../AccountList'
|
import AccountList from '../AccountList'
|
||||||
import BunkerLogin from './BunkerLogin'
|
import BunkerLogin from './BunkerLogin'
|
||||||
import PrivateKeyLogin from './PrivateKeyLogin'
|
|
||||||
import GenerateNewAccount from './GenerateNewAccount'
|
import GenerateNewAccount from './GenerateNewAccount'
|
||||||
|
import PrivateKeyLogin from './PrivateKeyLogin'
|
||||||
|
|
||||||
type TAccountManagerPage = 'nsec' | 'bunker' | 'generate' | null
|
type TAccountManagerPage = 'nsec' | 'bunker' | 'generate' | null
|
||||||
|
|
||||||
@@ -36,38 +37,74 @@ function AccountManagerNav({
|
|||||||
close?: () => void
|
close?: () => void
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { nip07Login, accounts } = useNostr()
|
const { nip07Login, bunkerLogin, nsecLogin, ncryptsecLogin, accounts } = useNostr()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div onClick={(e) => e.stopPropagation()} className="flex flex-col gap-4">
|
<div onClick={(e) => e.stopPropagation()} className="flex flex-col gap-8">
|
||||||
<div className="text-center text-muted-foreground text-sm font-semibold">
|
<div>
|
||||||
{t('Add an Account')}
|
<div className="text-center text-muted-foreground text-sm font-semibold">
|
||||||
|
{t('Add an Account')}
|
||||||
|
</div>
|
||||||
|
<div className="space-y-2 mt-4">
|
||||||
|
{!!window.nostr && (
|
||||||
|
<Button onClick={() => nip07Login().then(() => close?.())} className="w-full">
|
||||||
|
{t('Login with Browser Extension')}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button variant="secondary" onClick={() => setPage('bunker')} className="w-full">
|
||||||
|
{t('Login with Bunker')}
|
||||||
|
</Button>
|
||||||
|
<Button variant="secondary" onClick={() => setPage('nsec')} className="w-full">
|
||||||
|
{t('Login with Private Key')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!!window.nostr && (
|
|
||||||
<Button onClick={() => nip07Login().then(() => close?.())} className="w-full">
|
|
||||||
{t('Login with Browser Extension')}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
<Button variant="secondary" onClick={() => setPage('bunker')} className="w-full">
|
|
||||||
{t('Login with Bunker')}
|
|
||||||
</Button>
|
|
||||||
<Button variant="secondary" onClick={() => setPage('nsec')} className="w-full">
|
|
||||||
{t('Login with Private Key')}
|
|
||||||
</Button>
|
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="text-center text-muted-foreground text-sm font-semibold">
|
<div>
|
||||||
{t("Don't have an account yet?")}
|
<div className="text-center text-muted-foreground text-sm font-semibold">
|
||||||
|
{t("Don't have an account yet?")}
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
onClick={() => {
|
||||||
|
const wizard = new NstartModal({
|
||||||
|
baseUrl: 'https://start.njump.me',
|
||||||
|
an: 'Jumble',
|
||||||
|
onComplete: ({ nostrLogin }) => {
|
||||||
|
if (!nostrLogin) return
|
||||||
|
|
||||||
|
if (nostrLogin.startsWith('bunker://')) {
|
||||||
|
bunkerLogin(nostrLogin)
|
||||||
|
} else if (nostrLogin.startsWith('ncryptsec')) {
|
||||||
|
ncryptsecLogin(nostrLogin)
|
||||||
|
} else if (nostrLogin.startsWith('nsec')) {
|
||||||
|
nsecLogin(nostrLogin)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
close?.()
|
||||||
|
wizard.open()
|
||||||
|
}}
|
||||||
|
className="w-full mt-4"
|
||||||
|
>
|
||||||
|
{t('Signup with Nstart wizard')}
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() => setPage('generate')}
|
||||||
|
className="w-full text-muted-foreground py-0 h-fit mt-1"
|
||||||
|
>
|
||||||
|
{t('or generate your private key here')}
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="secondary" onClick={() => setPage('generate')} className="w-full">
|
|
||||||
{t('Generate New Account')}
|
|
||||||
</Button>
|
|
||||||
{accounts.length > 0 && (
|
{accounts.length > 0 && (
|
||||||
<>
|
<>
|
||||||
<Separator />
|
<Separator />
|
||||||
<div className="text-center text-muted-foreground text-sm font-semibold">
|
<div>
|
||||||
{t('Logged in Accounts')}
|
<div className="text-center text-muted-foreground text-sm font-semibold">
|
||||||
|
{t('Logged in Accounts')}
|
||||||
|
</div>
|
||||||
|
<AccountList className="mt-4" afterSwitch={() => close?.()} />
|
||||||
</div>
|
</div>
|
||||||
<AccountList afterSwitch={() => close?.()} />
|
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import PostEditor from '@/components/PostEditor'
|
import PostEditor from '@/components/PostEditor'
|
||||||
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import { PencilLine } from 'lucide-react'
|
import { PencilLine } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import BottomNavigationBarItem from './BottomNavigationBarItem'
|
import BottomNavigationBarItem from './BottomNavigationBarItem'
|
||||||
|
|
||||||
export default function PostButton() {
|
export default function PostButton() {
|
||||||
|
const { checkLogin } = useNostr()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -11,7 +13,9 @@ export default function PostButton() {
|
|||||||
<BottomNavigationBarItem
|
<BottomNavigationBarItem
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setOpen(true)
|
checkLogin(() => {
|
||||||
|
setOpen(true)
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PencilLine />
|
<PencilLine />
|
||||||
|
|||||||
@@ -1,10 +1,4 @@
|
|||||||
import {
|
import { Dialog, DialogContent } from '@/components/ui/dialog'
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle
|
|
||||||
} from '@/components/ui/dialog'
|
|
||||||
import { Drawer, DrawerContent } from '@/components/ui/drawer'
|
import { Drawer, DrawerContent } from '@/components/ui/drawer'
|
||||||
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
import { useScreenSize } from '@/providers/ScreenSizeProvider'
|
||||||
import { Dispatch } from 'react'
|
import { Dispatch } from 'react'
|
||||||
@@ -33,11 +27,7 @@ export default function LoginDialog({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={open} onOpenChange={setOpen}>
|
<Dialog open={open} onOpenChange={setOpen}>
|
||||||
<DialogContent className="w-[520px] max-h-[90vh] overflow-auto">
|
<DialogContent className="w-[520px] max-h-[90vh] py-8 overflow-auto">
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle className="hidden" />
|
|
||||||
<DialogDescription className="hidden" />
|
|
||||||
</DialogHeader>
|
|
||||||
<AccountManager close={() => setOpen(false)} />
|
<AccountManager close={() => setOpen(false)} />
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
import { useNoteStats } from '@/providers/NoteStatsProvider'
|
||||||
import { MessageCircle } from 'lucide-react'
|
import { MessageCircle } from 'lucide-react'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
@@ -8,6 +9,7 @@ import { formatCount } from './utils'
|
|||||||
|
|
||||||
export default function ReplyButton({ event }: { event: Event }) {
|
export default function ReplyButton({ event }: { event: Event }) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { checkLogin } = useNostr()
|
||||||
const { noteStatsMap } = useNoteStats()
|
const { noteStatsMap } = useNoteStats()
|
||||||
const { replyCount } = useMemo(() => noteStatsMap.get(event.id) ?? {}, [noteStatsMap, event.id])
|
const { replyCount } = useMemo(() => noteStatsMap.get(event.id) ?? {}, [noteStatsMap, event.id])
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
@@ -18,7 +20,9 @@ export default function ReplyButton({ event }: { event: Event }) {
|
|||||||
className="flex gap-1 items-center text-muted-foreground enabled:hover:text-blue-400"
|
className="flex gap-1 items-center text-muted-foreground enabled:hover:text-blue-400"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setOpen(true)
|
checkLogin(() => {
|
||||||
|
setOpen(true)
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
title={t('Reply')}
|
title={t('Reply')}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -94,7 +94,14 @@ export default function RepostButton({
|
|||||||
<DropdownMenuItem onClick={repost} disabled={!canRepost}>
|
<DropdownMenuItem onClick={repost} disabled={!canRepost}>
|
||||||
<Repeat /> {t('Repost')}
|
<Repeat /> {t('Repost')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem onClick={() => setIsPostDialogOpen(true)}>
|
<DropdownMenuItem
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
checkLogin(() => {
|
||||||
|
setIsPostDialogOpen(true)
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
>
|
||||||
<PencilLine /> {t('Quote')}
|
<PencilLine /> {t('Quote')}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { useSecondaryPage } from '@/PageManager'
|
import { useSecondaryPage } from '@/PageManager'
|
||||||
import { toNote } from '@/lib/link'
|
import { toNote } from '@/lib/link'
|
||||||
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import { Event } from 'nostr-tools'
|
import { Event } from 'nostr-tools'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
@@ -23,6 +24,7 @@ export default function ReplyNote({
|
|||||||
highlight?: boolean
|
highlight?: boolean
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const { checkLogin } = useNostr()
|
||||||
const { push } = useSecondaryPage()
|
const { push } = useSecondaryPage()
|
||||||
const [isPostDialogOpen, setIsPostDialogOpen] = useState(false)
|
const [isPostDialogOpen, setIsPostDialogOpen] = useState(false)
|
||||||
|
|
||||||
@@ -56,7 +58,7 @@ export default function ReplyNote({
|
|||||||
className="text-muted-foreground hover:text-primary cursor-pointer"
|
className="text-muted-foreground hover:text-primary cursor-pointer"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setIsPostDialogOpen(true)
|
checkLogin(() => setIsPostDialogOpen(true))
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('reply')}
|
{t('reply')}
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
import PostEditor from '@/components/PostEditor'
|
import PostEditor from '@/components/PostEditor'
|
||||||
|
import { useNostr } from '@/providers/NostrProvider'
|
||||||
import { PencilLine } from 'lucide-react'
|
import { PencilLine } from 'lucide-react'
|
||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import SidebarItem from './SidebarItem'
|
import SidebarItem from './SidebarItem'
|
||||||
|
|
||||||
export default function PostButton() {
|
export default function PostButton() {
|
||||||
|
const { checkLogin } = useNostr()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -13,7 +15,9 @@ export default function PostButton() {
|
|||||||
description="Post"
|
description="Post"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
setOpen(true)
|
checkLogin(() => {
|
||||||
|
setOpen(true)
|
||||||
|
})
|
||||||
}}
|
}}
|
||||||
variant="default"
|
variant="default"
|
||||||
className="bg-primary xl:justify-center gap-2"
|
className="bg-primary xl:justify-center gap-2"
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export default {
|
|||||||
'read & write relays notice':
|
'read & write relays notice':
|
||||||
'The number of read and write servers should ideally be kept between 2 and 4.',
|
'The number of read and write servers should ideally be kept between 2 and 4.',
|
||||||
"Don't have an account yet?": "Don't have an account yet?",
|
"Don't have an account yet?": "Don't have an account yet?",
|
||||||
'Generate New Account': 'Generate New Account',
|
'or generate your private key here': 'or generate your private key here',
|
||||||
'This is a private key. Do not share it with anyone. Keep it safe and secure. You will not be able to recover it if you lose it.':
|
'This is a private key. Do not share it with anyone. Keep it safe and secure. You will not be able to recover it if you lose it.':
|
||||||
'This is a private key. Do not share it with anyone. Keep it safe and secure. You will not be able to recover it if you lose it.',
|
'This is a private key. Do not share it with anyone. Keep it safe and secure. You will not be able to recover it if you lose it.',
|
||||||
Edit: 'Edit',
|
Edit: 'Edit',
|
||||||
@@ -146,6 +146,7 @@ export default {
|
|||||||
Back: 'Back',
|
Back: 'Back',
|
||||||
'optional: encrypt nsec': 'optional: encrypt nsec',
|
'optional: encrypt nsec': 'optional: encrypt nsec',
|
||||||
password: 'password',
|
password: 'password',
|
||||||
|
'Signup with Nstart wizard': 'Signup with Nstart wizard',
|
||||||
'Save to': 'Save to',
|
'Save to': 'Save to',
|
||||||
'Enter a name for the new relay set': 'Enter a name for the new relay set',
|
'Enter a name for the new relay set': 'Enter a name for the new relay set',
|
||||||
'Save to a new relay set': 'Save to a new relay set',
|
'Save to a new relay set': 'Save to a new relay set',
|
||||||
|
|||||||
@@ -132,7 +132,7 @@ export default {
|
|||||||
'写服务器用于发布您的事件。其他用户会从您的写服务器寻找您发布的事件。',
|
'写服务器用于发布您的事件。其他用户会从您的写服务器寻找您发布的事件。',
|
||||||
'read & write relays notice': '读服务器和写服务器的数量都应尽量保持在 2 到 4 个之间。',
|
'read & write relays notice': '读服务器和写服务器的数量都应尽量保持在 2 到 4 个之间。',
|
||||||
"Don't have an account yet?": '还没有账户?',
|
"Don't have an account yet?": '还没有账户?',
|
||||||
'Generate New Account': '生成新账户',
|
'or generate your private key here': '或者直接生成私钥',
|
||||||
'This is a private key. Do not share it with anyone. Keep it safe and secure. You will not be able to recover it if you lose it.':
|
'This is a private key. Do not share it with anyone. Keep it safe and secure. You will not be able to recover it if you lose it.':
|
||||||
'这是私钥,请勿与他人分享。请妥善保管,否则将无法找回。',
|
'这是私钥,请勿与他人分享。请妥善保管,否则将无法找回。',
|
||||||
Edit: '编辑',
|
Edit: '编辑',
|
||||||
@@ -147,6 +147,7 @@ export default {
|
|||||||
'password (optional): encrypt nsec': '密码 (可选): 加密 nsec',
|
'password (optional): encrypt nsec': '密码 (可选): 加密 nsec',
|
||||||
'optional: encrypt nsec': '可选: 加密 nsec',
|
'optional: encrypt nsec': '可选: 加密 nsec',
|
||||||
password: '密码',
|
password: '密码',
|
||||||
|
'Signup with Nstart wizard': '使用 Nstart 向导注册',
|
||||||
'Save to': '保存到',
|
'Save to': '保存到',
|
||||||
'Enter a name for the new relay set': '输入新服务器组的名称',
|
'Enter a name for the new relay set': '输入新服务器组的名称',
|
||||||
'Save to a new relay set': '保存到新服务器组',
|
'Save to a new relay set': '保存到新服务器组',
|
||||||
|
|||||||
@@ -69,6 +69,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const init = async () => {
|
const init = async () => {
|
||||||
|
if (hasNostrLoginHash()) {
|
||||||
|
return await loginByNostrLoginHash()
|
||||||
|
}
|
||||||
|
|
||||||
const accounts = storage.getAccounts()
|
const accounts = storage.getAccounts()
|
||||||
const act = storage.getCurrentAccount() ?? accounts[0] // auto login the first account
|
const act = storage.getCurrentAccount() ?? accounts[0] // auto login the first account
|
||||||
if (!act) return
|
if (!act) return
|
||||||
@@ -76,6 +80,18 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
await loginWithAccountPointer(act)
|
await loginWithAccountPointer(act)
|
||||||
}
|
}
|
||||||
init()
|
init()
|
||||||
|
|
||||||
|
const handleHashChange = () => {
|
||||||
|
if (hasNostrLoginHash()) {
|
||||||
|
loginByNostrLoginHash()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
window.addEventListener('hashchange', handleHashChange)
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('hashchange', handleHashChange)
|
||||||
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -138,6 +154,24 @@ export function NostrProvider({ children }: { children: React.ReactNode }) {
|
|||||||
})
|
})
|
||||||
}, [account])
|
}, [account])
|
||||||
|
|
||||||
|
const hasNostrLoginHash = () => {
|
||||||
|
return window.location.hash && window.location.hash.startsWith('#nostr-login')
|
||||||
|
}
|
||||||
|
|
||||||
|
const loginByNostrLoginHash = async () => {
|
||||||
|
const credential = window.location.hash.replace('#nostr-login=', '')
|
||||||
|
const urlWithoutHash = window.location.href.split('#')[0]
|
||||||
|
history.replaceState(null, '', urlWithoutHash)
|
||||||
|
|
||||||
|
if (credential.startsWith('bunker://')) {
|
||||||
|
return await bunkerLogin(credential)
|
||||||
|
} else if (credential.startsWith('ncryptsec')) {
|
||||||
|
return await ncryptsecLogin(credential)
|
||||||
|
} else if (credential.startsWith('nsec')) {
|
||||||
|
return await nsecLogin(credential)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const login = (signer: ISigner, act: TAccount) => {
|
const login = (signer: ISigner, act: TAccount) => {
|
||||||
storage.addAccount(act)
|
storage.addAccount(act)
|
||||||
storage.switchAccount(act)
|
storage.switchAccount(act)
|
||||||
|
|||||||
Reference in New Issue
Block a user