From a264b747e7c190b76a0b3da43852ea6db65b5409 Mon Sep 17 00:00:00 2001 From: Cody Tseng Date: Wed, 29 Jan 2025 15:32:26 +0800 Subject: [PATCH] feat: integrate nstart (#33) --- .gitignore | 1 + package-lock.json | 6 ++ package.json | 1 + src/PageManager.tsx | 13 ++- src/components/AccountList/index.tsx | 10 ++- src/components/AccountManager/index.tsx | 85 +++++++++++++------ .../BottomNavigationBar/PostButton.tsx | 6 +- src/components/LoginDialog/index.tsx | 14 +-- src/components/NoteStats/ReplyButton.tsx | 6 +- src/components/NoteStats/RepostButton.tsx | 9 +- src/components/ReplyNote/index.tsx | 4 +- src/components/Sidebar/PostButton.tsx | 6 +- src/i18n/en.ts | 3 +- src/i18n/zh.ts | 3 +- src/providers/NostrProvider/index.tsx | 34 ++++++++ 15 files changed, 154 insertions(+), 47 deletions(-) diff --git a/.gitignore b/.gitignore index 6d6ae5a..692c42a 100644 --- a/.gitignore +++ b/.gitignore @@ -12,6 +12,7 @@ dist dist-ssr dev-dist *.local +.env # Editor directories and files .vscode/* diff --git a/package-lock.json b/package-lock.json index fcb8207..a819cc6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -35,6 +35,7 @@ "lru-cache": "^11.0.2", "lucide-react": "^0.469.0", "nostr-tools": "^2.10.4", + "nstart-modal": "^0.2.0", "path-to-regexp": "^8.2.0", "qrcode.react": "^4.2.0", "react": "^18.3.1", @@ -7011,6 +7012,11 @@ "integrity": "sha512-78BTryCLcLYv96ONU8Ws3Q1JzjlAt+43pWQhIl86xZmWeegYCNLPml7yQ+gG3vR6V5h4XGj+TxO+SS5dsThQIA==", "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": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", diff --git a/package.json b/package.json index 995919d..4ce7249 100644 --- a/package.json +++ b/package.json @@ -45,6 +45,7 @@ "lru-cache": "^11.0.2", "lucide-react": "^0.469.0", "nostr-tools": "^2.10.4", + "nstart-modal": "^0.2.0", "path-to-regexp": "^8.2.0", "qrcode.react": "^4.2.0", "react": "^18.3.1", diff --git a/src/PageManager.tsx b/src/PageManager.tsx index b8b5af9..2799c49 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -105,10 +105,19 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } const onPopState = (e: PopStateEvent) => { - const state = e.state ?? { index: -1, url: '/' } + let state = e.state as { index: number; url: string } | null setSecondaryStack((pre) => { const currentItem = pre[pre.length - 1] as TStackItem | undefined 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 if (currentIndex === undefined || state.index > currentIndex) { @@ -124,7 +133,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { } // 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 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) diff --git a/src/components/AccountList/index.tsx b/src/components/AccountList/index.tsx index be2eb9b..712d0c8 100644 --- a/src/components/AccountList/index.tsx +++ b/src/components/AccountList/index.tsx @@ -9,12 +9,18 @@ import { useState } from 'react' import { SimpleUserAvatar } from '../UserAvatar' 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 [switchingAccount, setSwitchingAccount] = useState(null) return ( -
+
{accounts.map((act) => (
void }) { const { t } = useTranslation() - const { nip07Login, accounts } = useNostr() + const { nip07Login, bunkerLogin, nsecLogin, ncryptsecLogin, accounts } = useNostr() return ( -
e.stopPropagation()} className="flex flex-col gap-4"> -
- {t('Add an Account')} +
e.stopPropagation()} className="flex flex-col gap-8"> +
+
+ {t('Add an Account')} +
+
+ {!!window.nostr && ( + + )} + + +
- {!!window.nostr && ( - - )} - - -
- {t("Don't have an account yet?")} +
+
+ {t("Don't have an account yet?")} +
+ +
- {accounts.length > 0 && ( <> -
- {t('Logged in Accounts')} +
+
+ {t('Logged in Accounts')} +
+ close?.()} />
- close?.()} /> )}
diff --git a/src/components/BottomNavigationBar/PostButton.tsx b/src/components/BottomNavigationBar/PostButton.tsx index 10ee511..eb58710 100644 --- a/src/components/BottomNavigationBar/PostButton.tsx +++ b/src/components/BottomNavigationBar/PostButton.tsx @@ -1,9 +1,11 @@ import PostEditor from '@/components/PostEditor' +import { useNostr } from '@/providers/NostrProvider' import { PencilLine } from 'lucide-react' import { useState } from 'react' import BottomNavigationBarItem from './BottomNavigationBarItem' export default function PostButton() { + const { checkLogin } = useNostr() const [open, setOpen] = useState(false) return ( @@ -11,7 +13,9 @@ export default function PostButton() { { e.stopPropagation() - setOpen(true) + checkLogin(() => { + setOpen(true) + }) }} > diff --git a/src/components/LoginDialog/index.tsx b/src/components/LoginDialog/index.tsx index af1f925..94ca698 100644 --- a/src/components/LoginDialog/index.tsx +++ b/src/components/LoginDialog/index.tsx @@ -1,10 +1,4 @@ -import { - Dialog, - DialogContent, - DialogDescription, - DialogHeader, - DialogTitle -} from '@/components/ui/dialog' +import { Dialog, DialogContent } from '@/components/ui/dialog' import { Drawer, DrawerContent } from '@/components/ui/drawer' import { useScreenSize } from '@/providers/ScreenSizeProvider' import { Dispatch } from 'react' @@ -33,11 +27,7 @@ export default function LoginDialog({ return ( - - - - - + setOpen(false)} /> diff --git a/src/components/NoteStats/ReplyButton.tsx b/src/components/NoteStats/ReplyButton.tsx index 3e78f45..ed6a658 100644 --- a/src/components/NoteStats/ReplyButton.tsx +++ b/src/components/NoteStats/ReplyButton.tsx @@ -1,3 +1,4 @@ +import { useNostr } from '@/providers/NostrProvider' import { useNoteStats } from '@/providers/NoteStatsProvider' import { MessageCircle } from 'lucide-react' import { Event } from 'nostr-tools' @@ -8,6 +9,7 @@ import { formatCount } from './utils' export default function ReplyButton({ event }: { event: Event }) { const { t } = useTranslation() + const { checkLogin } = useNostr() const { noteStatsMap } = useNoteStats() const { replyCount } = useMemo(() => noteStatsMap.get(event.id) ?? {}, [noteStatsMap, event.id]) 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" onClick={(e) => { e.stopPropagation() - setOpen(true) + checkLogin(() => { + setOpen(true) + }) }} title={t('Reply')} > diff --git a/src/components/NoteStats/RepostButton.tsx b/src/components/NoteStats/RepostButton.tsx index 2bf85e4..c74008b 100644 --- a/src/components/NoteStats/RepostButton.tsx +++ b/src/components/NoteStats/RepostButton.tsx @@ -94,7 +94,14 @@ export default function RepostButton({ {t('Repost')} - setIsPostDialogOpen(true)}> + { + e.stopPropagation() + checkLogin(() => { + setIsPostDialogOpen(true) + }) + }} + > {t('Quote')} diff --git a/src/components/ReplyNote/index.tsx b/src/components/ReplyNote/index.tsx index e642a9c..5a3c6cf 100644 --- a/src/components/ReplyNote/index.tsx +++ b/src/components/ReplyNote/index.tsx @@ -1,5 +1,6 @@ import { useSecondaryPage } from '@/PageManager' import { toNote } from '@/lib/link' +import { useNostr } from '@/providers/NostrProvider' import { Event } from 'nostr-tools' import { useState } from 'react' import { useTranslation } from 'react-i18next' @@ -23,6 +24,7 @@ export default function ReplyNote({ highlight?: boolean }) { const { t } = useTranslation() + const { checkLogin } = useNostr() const { push } = useSecondaryPage() const [isPostDialogOpen, setIsPostDialogOpen] = useState(false) @@ -56,7 +58,7 @@ export default function ReplyNote({ className="text-muted-foreground hover:text-primary cursor-pointer" onClick={(e) => { e.stopPropagation() - setIsPostDialogOpen(true) + checkLogin(() => setIsPostDialogOpen(true)) }} > {t('reply')} diff --git a/src/components/Sidebar/PostButton.tsx b/src/components/Sidebar/PostButton.tsx index 5cc8ea1..39258a5 100644 --- a/src/components/Sidebar/PostButton.tsx +++ b/src/components/Sidebar/PostButton.tsx @@ -1,9 +1,11 @@ import PostEditor from '@/components/PostEditor' +import { useNostr } from '@/providers/NostrProvider' import { PencilLine } from 'lucide-react' import { useState } from 'react' import SidebarItem from './SidebarItem' export default function PostButton() { + const { checkLogin } = useNostr() const [open, setOpen] = useState(false) return ( @@ -13,7 +15,9 @@ export default function PostButton() { description="Post" onClick={(e) => { e.stopPropagation() - setOpen(true) + checkLogin(() => { + setOpen(true) + }) }} variant="default" className="bg-primary xl:justify-center gap-2" diff --git a/src/i18n/en.ts b/src/i18n/en.ts index 4318fc9..0473309 100644 --- a/src/i18n/en.ts +++ b/src/i18n/en.ts @@ -132,7 +132,7 @@ export default { 'read & write relays notice': '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?", - '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.', Edit: 'Edit', @@ -146,6 +146,7 @@ export default { Back: 'Back', 'optional: encrypt nsec': 'optional: encrypt nsec', password: 'password', + 'Signup with Nstart wizard': 'Signup with Nstart wizard', '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', diff --git a/src/i18n/zh.ts b/src/i18n/zh.ts index 9838102..f8f7231 100644 --- a/src/i18n/zh.ts +++ b/src/i18n/zh.ts @@ -132,7 +132,7 @@ export default { '写服务器用于发布您的事件。其他用户会从您的写服务器寻找您发布的事件。', 'read & write relays notice': '读服务器和写服务器的数量都应尽量保持在 2 到 4 个之间。', "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.': '这是私钥,请勿与他人分享。请妥善保管,否则将无法找回。', Edit: '编辑', @@ -147,6 +147,7 @@ export default { 'password (optional): encrypt nsec': '密码 (可选): 加密 nsec', 'optional: encrypt nsec': '可选: 加密 nsec', password: '密码', + 'Signup with Nstart wizard': '使用 Nstart 向导注册', 'Save to': '保存到', 'Enter a name for the new relay set': '输入新服务器组的名称', 'Save to a new relay set': '保存到新服务器组', diff --git a/src/providers/NostrProvider/index.tsx b/src/providers/NostrProvider/index.tsx index bea0eb4..414ef54 100644 --- a/src/providers/NostrProvider/index.tsx +++ b/src/providers/NostrProvider/index.tsx @@ -69,6 +69,10 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { useEffect(() => { const init = async () => { + if (hasNostrLoginHash()) { + return await loginByNostrLoginHash() + } + const accounts = storage.getAccounts() const act = storage.getCurrentAccount() ?? accounts[0] // auto login the first account if (!act) return @@ -76,6 +80,18 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { await loginWithAccountPointer(act) } init() + + const handleHashChange = () => { + if (hasNostrLoginHash()) { + loginByNostrLoginHash() + } + } + + window.addEventListener('hashchange', handleHashChange) + + return () => { + window.removeEventListener('hashchange', handleHashChange) + } }, []) useEffect(() => { @@ -138,6 +154,24 @@ export function NostrProvider({ children }: { children: React.ReactNode }) { }) }, [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) => { storage.addAccount(act) storage.switchAccount(act)