From ad6b8890c578629cbd55a492e8ecef2f525312f6 Mon Sep 17 00:00:00 2001 From: codytseng Date: Sun, 26 Oct 2025 16:11:21 +0800 Subject: [PATCH] feat: add quick account switch interaction --- src/components/AccountList/index.tsx | 18 +---- .../BottomNavigationBar/AccountButton.tsx | 59 +++++++++----- .../BottomNavigationBarItem.tsx | 12 ++- src/components/NoteStats/ZapButton.tsx | 3 +- src/components/Sidebar/AccountButton.tsx | 81 +++++++++++++------ src/components/SignerTypeBadge/index.tsx | 23 ++++++ src/components/Username/index.tsx | 8 +- src/constants.ts | 2 + src/i18n/locales/ar.ts | 6 +- src/i18n/locales/de.ts | 6 +- src/i18n/locales/en.ts | 6 +- src/i18n/locales/es.ts | 6 +- src/i18n/locales/fa.ts | 6 +- src/i18n/locales/fr.ts | 6 +- src/i18n/locales/hi.ts | 6 +- src/i18n/locales/it.ts | 6 +- src/i18n/locales/ja.ts | 6 +- src/i18n/locales/ko.ts | 6 +- src/i18n/locales/pl.ts | 6 +- src/i18n/locales/pt-BR.ts | 6 +- src/i18n/locales/pt-PT.ts | 6 +- src/i18n/locales/ru.ts | 6 +- src/i18n/locales/th.ts | 6 +- src/i18n/locales/zh.ts | 6 +- 24 files changed, 217 insertions(+), 85 deletions(-) create mode 100644 src/components/SignerTypeBadge/index.tsx diff --git a/src/components/AccountList/index.tsx b/src/components/AccountList/index.tsx index 2385c397..7145789b 100644 --- a/src/components/AccountList/index.tsx +++ b/src/components/AccountList/index.tsx @@ -1,12 +1,12 @@ -import { Badge } from '@/components/ui/badge' import { Button } from '@/components/ui/button' import { isSameAccount } from '@/lib/account' import { formatPubkey } from '@/lib/pubkey' import { cn } from '@/lib/utils' import { useNostr } from '@/providers/NostrProvider' -import { TAccountPointer, TSignerType } from '@/types' +import { TAccountPointer } from '@/types' import { Loader, Trash2 } from 'lucide-react' import { useState } from 'react' +import SignerTypeBadge from '../SignerTypeBadge' import { SimpleUserAvatar } from '../UserAvatar' import { SimpleUsername } from '../Username' @@ -74,17 +74,3 @@ export default function AccountList({ ) } - -function SignerTypeBadge({ signerType }: { signerType: TSignerType }) { - if (signerType === 'nip-07') { - return NIP-07 - } else if (signerType === 'bunker') { - return Bunker - } else if (signerType === 'ncryptsec') { - return NCRYPTSEC - } else if (signerType === 'nsec') { - return NSEC - } else if (signerType === 'npub') { - return NPUB - } -} diff --git a/src/components/BottomNavigationBar/AccountButton.tsx b/src/components/BottomNavigationBar/AccountButton.tsx index db8e9e40..02aec793 100644 --- a/src/components/BottomNavigationBar/AccountButton.tsx +++ b/src/components/BottomNavigationBar/AccountButton.tsx @@ -1,36 +1,57 @@ import { Skeleton } from '@/components/ui/skeleton' +import { LONG_PRESS_THRESHOLD } from '@/constants' import { cn } from '@/lib/utils' import { usePrimaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' import { UserRound } from 'lucide-react' -import { useMemo } from 'react' +import { useMemo, useRef, useState } from 'react' +import LoginDialog from '../LoginDialog' import { SimpleUserAvatar } from '../UserAvatar' import BottomNavigationBarItem from './BottomNavigationBarItem' export default function AccountButton() { const { navigate, current, display } = usePrimaryPage() const { pubkey, profile } = useNostr() + const [loginDialogOpen, setLoginDialogOpen] = useState(false) const active = useMemo(() => current === 'me' && display, [display, current]) + const pressTimerRef = useRef | null>(null) + + const handlePointerDown = () => { + pressTimerRef.current = setTimeout(() => { + setLoginDialogOpen(true) + pressTimerRef.current = null + }, LONG_PRESS_THRESHOLD) + } + + const handlePointerUp = () => { + if (pressTimerRef.current) { + clearTimeout(pressTimerRef.current) + navigate('me') + pressTimerRef.current = null + } + } return ( - { - navigate('me') - }} - active={active} - > - {pubkey ? ( - profile ? ( - + <> + + {pubkey ? ( + profile ? ( + + ) : ( + + ) ) : ( - - ) - ) : ( - - )} - + + )} + + + ) } diff --git a/src/components/BottomNavigationBar/BottomNavigationBarItem.tsx b/src/components/BottomNavigationBar/BottomNavigationBarItem.tsx index 16c79927..16fdc997 100644 --- a/src/components/BottomNavigationBar/BottomNavigationBarItem.tsx +++ b/src/components/BottomNavigationBar/BottomNavigationBarItem.tsx @@ -1,15 +1,19 @@ +import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' -import { Button } from '../ui/button' import { MouseEventHandler } from 'react' export default function BottomNavigationBarItem({ children, active = false, - onClick + onClick, + onPointerDown, + onPointerUp }: { children: React.ReactNode active?: boolean - onClick: MouseEventHandler + onClick?: MouseEventHandler + onPointerDown?: MouseEventHandler + onPointerUp?: MouseEventHandler }) { return ( diff --git a/src/components/NoteStats/ZapButton.tsx b/src/components/NoteStats/ZapButton.tsx index 24ef403d..0e9325f9 100644 --- a/src/components/NoteStats/ZapButton.tsx +++ b/src/components/NoteStats/ZapButton.tsx @@ -1,3 +1,4 @@ +import { LONG_PRESS_THRESHOLD } from '@/constants' import { useNoteStatsById } from '@/hooks/useNoteStatsById' import { getLightningAddressFromProfile } from '@/lib/lightning' import { cn } from '@/lib/utils' @@ -86,7 +87,7 @@ export default function ZapButton({ event }: { event: Event }) { setOpenZapDialog(true) setZapping(true) }) - }, 500) + }, LONG_PRESS_THRESHOLD) } } diff --git a/src/components/Sidebar/AccountButton.tsx b/src/components/Sidebar/AccountButton.tsx index c187049c..26e905f1 100644 --- a/src/components/Sidebar/AccountButton.tsx +++ b/src/components/Sidebar/AccountButton.tsx @@ -1,22 +1,24 @@ -import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' import { Button } from '@/components/ui/button' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, + DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' import { toWallet } from '@/lib/link' -import { formatPubkey, generateImageByPubkey } from '@/lib/pubkey' import { cn } from '@/lib/utils' -import { usePrimaryPage, useSecondaryPage } from '@/PageManager' +import { useSecondaryPage } from '@/PageManager' import { useNostr } from '@/providers/NostrProvider' -import { ArrowDownUp, LogIn, LogOut, UserRound, Wallet } from 'lucide-react' +import { LogIn, LogOut, Plus, Wallet } from 'lucide-react' import { useState } from 'react' import { useTranslation } from 'react-i18next' import LoginDialog from '../LoginDialog' import LogoutDialog from '../LogoutDialog' +import SignerTypeBadge from '../SignerTypeBadge' +import { SimpleUserAvatar } from '../UserAvatar' +import { SimpleUsername } from '../Username' import SidebarItem from './SidebarItem' export default function AccountButton({ collapse }: { collapse: boolean }) { @@ -31,17 +33,13 @@ export default function AccountButton({ collapse }: { collapse: boolean }) { function ProfileButton({ collapse }: { collapse: boolean }) { const { t } = useTranslation() - const { account, profile } = useNostr() + const { account, accounts, switchAccount } = useNostr() const pubkey = account?.pubkey - const { navigate } = usePrimaryPage() const { push } = useSecondaryPage() const [loginDialogOpen, setLoginDialogOpen] = useState(false) const [logoutDialogOpen, setLogoutDialogOpen] = useState(false) if (!pubkey) return null - const defaultAvatar = generateImageByPubkey(pubkey) - const { username, avatar } = profile || { username: formatPubkey(pubkey), avatar: defaultAvatar } - return ( @@ -53,37 +51,68 @@ function ProfileButton({ collapse }: { collapse: boolean }) { )} >
- - - - - - - {!collapse &&
{username}
} + + {!collapse && ( + + )}
- - navigate('profile')}> - - {t('Profile')} - - + push(toWallet())}> {t('Wallet')} - setLoginDialogOpen(true)}> - - {t('Switch account')} + {t('Switch account')} + {accounts.map((act) => ( + { + if (act.pubkey !== pubkey) { + switchAccount(act) + } + }} + > +
+ +
+ + +
+
+
+ + ))} + setLoginDialogOpen(true)} + className="border border-dashed m-2 focus:border-muted-foreground focus:bg-background" + > +
+ + {t('Add an Account')} +
setLogoutDialogOpen(true)} > - {t('Logout')} + {t('Logout')} + diff --git a/src/components/SignerTypeBadge/index.tsx b/src/components/SignerTypeBadge/index.tsx new file mode 100644 index 00000000..1117c86e --- /dev/null +++ b/src/components/SignerTypeBadge/index.tsx @@ -0,0 +1,23 @@ +import { Badge } from '@/components/ui/badge' +import { TSignerType } from '@/types' +import { useTranslation } from 'react-i18next' + +export default function SignerTypeBadge({ signerType }: { signerType: TSignerType }) { + const { t } = useTranslation() + + if (signerType === 'nip-07') { + return {t('Extension')} + } else if (signerType === 'bunker') { + return {t('Remote')} + } else if (signerType === 'ncryptsec') { + return ( + {t('Encrypted Key')} + ) + } else if (signerType === 'nsec') { + return ( + {t('Private Key')} + ) + } else if (signerType === 'npub') { + return NPUB + } +} diff --git a/src/components/Username/index.tsx b/src/components/Username/index.tsx index 470757e0..6213c20e 100644 --- a/src/components/Username/index.tsx +++ b/src/components/Username/index.tsx @@ -19,8 +19,8 @@ export default function Username({ skeletonClassName?: string withoutSkeleton?: boolean }) { - const { profile } = useFetchProfile(userId) - if (!profile && !withoutSkeleton) { + const { profile, isFetching } = useFetchProfile(userId) + if (!profile && isFetching && !withoutSkeleton) { return (
@@ -63,8 +63,8 @@ export function SimpleUsername({ skeletonClassName?: string withoutSkeleton?: boolean }) { - const { profile } = useFetchProfile(userId) - if (!profile && !withoutSkeleton) { + const { profile, isFetching } = useFetchProfile(userId) + if (!profile && isFetching && !withoutSkeleton) { return (
diff --git a/src/constants.ts b/src/constants.ts index de3f238e..d52c1dad 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -432,3 +432,5 @@ export const PRIMARY_COLORS = { } } as const export type TPrimaryColor = keyof typeof PRIMARY_COLORS + +export const LONG_PRESS_THRESHOLD = 500 diff --git a/src/i18n/locales/ar.ts b/src/i18n/locales/ar.ts index 25f7fa01..9d5ee621 100644 --- a/src/i18n/locales/ar.ts +++ b/src/i18n/locales/ar.ts @@ -480,6 +480,10 @@ export default { Layout: 'التخطيط', 'Two-column': 'عمودين', 'Single-column': 'عمود واحد', - Reviews: 'المراجعات' + Reviews: 'المراجعات', + Extension: 'امتداد', + Remote: 'عن بُعد', + 'Encrypted Key': 'مفتاح مشفر', + 'Private Key': 'مفتاح خاص' } } diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 556f523b..b72e4e53 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -494,6 +494,10 @@ export default { Layout: 'Layout', 'Two-column': 'Zweispaltig', 'Single-column': 'Einspaltig', - Reviews: 'Bewertungen' + Reviews: 'Bewertungen', + Extension: 'Erweiterung', + Remote: 'Remote', + 'Encrypted Key': 'Verschlüsselter Schlüssel', + 'Private Key': 'Privater Schlüssel' } } diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 95ac13ff..217402a1 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -479,6 +479,10 @@ export default { Layout: 'Layout', 'Two-column': 'Two-column', 'Single-column': 'Single-column', - Reviews: 'Reviews' + Reviews: 'Reviews', + Extension: 'Extension', + Remote: 'Remote', + 'Encrypted Key': 'Encrypted Key', + 'Private Key': 'Private Key' } } diff --git a/src/i18n/locales/es.ts b/src/i18n/locales/es.ts index 1f1fbe51..08eebea5 100644 --- a/src/i18n/locales/es.ts +++ b/src/i18n/locales/es.ts @@ -488,6 +488,10 @@ export default { Layout: 'Diseño', 'Two-column': 'Doble columna', 'Single-column': 'Columna única', - Reviews: 'Reseñas' + Reviews: 'Reseñas', + Extension: 'Extensión', + Remote: 'Remoto', + 'Encrypted Key': 'Clave privada cifrada', + 'Private Key': 'Clave privada' } } diff --git a/src/i18n/locales/fa.ts b/src/i18n/locales/fa.ts index 29e1845b..4a13c6bd 100644 --- a/src/i18n/locales/fa.ts +++ b/src/i18n/locales/fa.ts @@ -483,6 +483,10 @@ export default { Layout: 'چیدمان', 'Two-column': 'دو ستونی', 'Single-column': 'تک ستونی', - Reviews: 'نقدها' + Reviews: 'نقدها', + Extension: 'افزونه', + Remote: 'از راه دور', + 'Encrypted Key': 'رمزگذاری شده کلید', + 'Private Key': 'کلید خصوصی' } } diff --git a/src/i18n/locales/fr.ts b/src/i18n/locales/fr.ts index acf3de55..b192800a 100644 --- a/src/i18n/locales/fr.ts +++ b/src/i18n/locales/fr.ts @@ -493,6 +493,10 @@ export default { Layout: 'Disposition', 'Two-column': 'Deux colonnes', 'Single-column': 'Une seule colonne', - Reviews: 'Avis' + Reviews: 'Avis', + Extension: 'Extension', + Remote: 'Distant', + 'Encrypted Key': 'Clé chiffrée', + 'Private Key': 'Clé privée' } } diff --git a/src/i18n/locales/hi.ts b/src/i18n/locales/hi.ts index a3f81049..ffdb9f0a 100644 --- a/src/i18n/locales/hi.ts +++ b/src/i18n/locales/hi.ts @@ -485,6 +485,10 @@ export default { Layout: 'लेआउट', 'Two-column': 'दोहरा स्तंभ', 'Single-column': 'एकल स्तंभ', - Reviews: 'समीक्षाएं' + Reviews: 'समीक्षाएं', + Extension: 'एक्सटेंशन', + Remote: 'रिमोट', + 'Encrypted Key': 'एन्क्रिप्टेड की', + 'Private Key': 'प्राइवेट की' } } diff --git a/src/i18n/locales/it.ts b/src/i18n/locales/it.ts index 16e5618f..c6db57a5 100644 --- a/src/i18n/locales/it.ts +++ b/src/i18n/locales/it.ts @@ -488,6 +488,10 @@ export default { Layout: 'Layout', 'Two-column': 'Doppia colonna', 'Single-column': 'Colonna singola', - Reviews: 'Recensioni' + Reviews: 'Recensioni', + Extension: 'Estensione', + Remote: 'Remoto', + 'Encrypted Key': 'Chiave Crittografata', + 'Private Key': 'Chiave Privata' } } diff --git a/src/i18n/locales/ja.ts b/src/i18n/locales/ja.ts index 6458dfa8..badcc137 100644 --- a/src/i18n/locales/ja.ts +++ b/src/i18n/locales/ja.ts @@ -484,6 +484,10 @@ export default { Layout: 'レイアウト', 'Two-column': '2列', 'Single-column': '1列', - Reviews: 'レビュー' + Reviews: 'レビュー', + Extension: '拡張機能', + Remote: 'リモート', + 'Encrypted Key': '暗号化キー', + 'Private Key': '暗号化されたキー' } } diff --git a/src/i18n/locales/ko.ts b/src/i18n/locales/ko.ts index dcc953c0..679319a5 100644 --- a/src/i18n/locales/ko.ts +++ b/src/i18n/locales/ko.ts @@ -484,6 +484,10 @@ export default { Layout: '레이아웃', 'Two-column': '두 열', 'Single-column': '한 열', - Reviews: '리뷰' + Reviews: '리뷰', + Extension: '확장 프로그램', + Remote: '원격', + 'Encrypted Key': '암호화된 키', + 'Private Key': '개인 키' } } diff --git a/src/i18n/locales/pl.ts b/src/i18n/locales/pl.ts index bfce50c9..7bba0345 100644 --- a/src/i18n/locales/pl.ts +++ b/src/i18n/locales/pl.ts @@ -488,6 +488,10 @@ export default { Layout: 'Układ', 'Two-column': 'Dwie kolumny', 'Single-column': 'Jedna kolumna', - Reviews: 'Opinie' + Reviews: 'Opinie', + Extension: 'Rozszerzenie', + Remote: 'Zdalne', + 'Encrypted Key': 'Zaszyfrowany Klucz', + 'Private Key': 'Zaszyfrowany Klucz' } } diff --git a/src/i18n/locales/pt-BR.ts b/src/i18n/locales/pt-BR.ts index 4bac4dad..d2fa8a91 100644 --- a/src/i18n/locales/pt-BR.ts +++ b/src/i18n/locales/pt-BR.ts @@ -485,6 +485,10 @@ export default { Layout: 'Layout', 'Two-column': 'Coluna dupla', 'Single-column': 'Coluna única', - Reviews: 'Avaliações' + Reviews: 'Avaliações', + Extension: 'Extensão', + Remote: 'Remoto', + 'Encrypted Key': 'Chave Criptografada', + 'Private Key': 'Chave Privada' } } diff --git a/src/i18n/locales/pt-PT.ts b/src/i18n/locales/pt-PT.ts index 2ed6abfb..dc4dc715 100644 --- a/src/i18n/locales/pt-PT.ts +++ b/src/i18n/locales/pt-PT.ts @@ -488,6 +488,10 @@ export default { Layout: 'Layout', 'Two-column': 'Coluna dupla', 'Single-column': 'Coluna única', - Reviews: 'Avaliações' + Reviews: 'Avaliações', + Extension: 'Extensão', + Remote: 'Remoto', + 'Encrypted Key': 'Chave Criptografada', + 'Private Key': 'Chave Privada' } } diff --git a/src/i18n/locales/ru.ts b/src/i18n/locales/ru.ts index 5549a761..5aadbd3a 100644 --- a/src/i18n/locales/ru.ts +++ b/src/i18n/locales/ru.ts @@ -490,6 +490,10 @@ export default { Layout: 'Макет', 'Two-column': 'Две колонки', 'Single-column': 'Одна колонка', - Reviews: 'Отзывы' + Reviews: 'Отзывы', + Extension: 'Расширение', + Remote: 'Удалённый', + 'Encrypted Key': 'Зашифрованный ключ', + 'Private Key': 'Приватный ключ' } } diff --git a/src/i18n/locales/th.ts b/src/i18n/locales/th.ts index c2f6693e..46285b7b 100644 --- a/src/i18n/locales/th.ts +++ b/src/i18n/locales/th.ts @@ -478,6 +478,10 @@ export default { Layout: 'เค้าโครง', 'Two-column': 'สองคอลัมน์', 'Single-column': 'คอลัมน์เดียว', - Reviews: 'รีวิว' + Reviews: 'รีวิว', + Extension: 'ส่วนขยาย', + Remote: 'ระยะไกล', + 'Encrypted Key': 'คีย์ที่เข้ารหัส', + 'Private Key': 'คีย์ส่วนตัว' } } diff --git a/src/i18n/locales/zh.ts b/src/i18n/locales/zh.ts index 142b61ca..4d5f20f5 100644 --- a/src/i18n/locales/zh.ts +++ b/src/i18n/locales/zh.ts @@ -476,6 +476,10 @@ export default { Layout: '布局', 'Two-column': '双栏', 'Single-column': '单栏', - Reviews: '评价' + Reviews: '评价', + Extension: '扩展', + Remote: '远程', + 'Encrypted Key': '加密私钥', + 'Private Key': '私钥' } }