8 changed files with 263 additions and 184 deletions
@ -0,0 +1,207 @@
@@ -0,0 +1,207 @@
|
||||
import LoginDialog from '@/components/LoginDialog' |
||||
import LogoutDialog from '@/components/LogoutDialog' |
||||
import { KeyboardShortcutsHelpButton } from '@/components/KeyboardShortcutsHelp' |
||||
import KeyboardShortcutsHelpSidebarButton from '@/components/Sidebar/KeyboardShortcutsHelpSidebarButton' |
||||
import SidebarItem from '@/components/Sidebar/SidebarItem' |
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' |
||||
import { Button } from '@/components/ui/button' |
||||
import { |
||||
DropdownMenu, |
||||
DropdownMenuContent, |
||||
DropdownMenuItem, |
||||
DropdownMenuSeparator, |
||||
DropdownMenuTrigger |
||||
} from '@/components/ui/dropdown-menu' |
||||
import { Skeleton } from '@/components/ui/skeleton' |
||||
import { formatPubkey, formatNpub, generateImageByPubkey, pubkeyToNpub } from '@/lib/pubkey' |
||||
import { cn } from '@/lib/utils' |
||||
import { usePrimaryPage } from '@/PageManager' |
||||
import { useNostr } from '@/providers/NostrProvider' |
||||
import { ArrowDownUp, LogIn, LogOut, Settings, User, UserRound } from 'lucide-react' |
||||
import { useMemo, useState, type ReactNode } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
|
||||
export type HelpAndAccountMenuVariant = 'sidebar' | 'titlebar' |
||||
|
||||
function AccountDropdownItems({ |
||||
onSwitchAccount, |
||||
onLogoutClick |
||||
}: { |
||||
onSwitchAccount: () => void |
||||
onLogoutClick: () => void |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const { navigate } = usePrimaryPage() |
||||
|
||||
return ( |
||||
<> |
||||
<DropdownMenuItem onClick={() => navigate('profile')}> |
||||
<User className="size-4" /> |
||||
{t('Profile')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem onClick={() => navigate('settings')}> |
||||
<Settings className="size-4" /> |
||||
{t('Settings')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuSeparator /> |
||||
<DropdownMenuItem onClick={onSwitchAccount}> |
||||
<ArrowDownUp className="size-4" /> |
||||
{t('Switch account')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem className="text-destructive focus:text-destructive" onClick={onLogoutClick}> |
||||
<LogOut className="size-4" /> |
||||
{t('Logout')} |
||||
</DropdownMenuItem> |
||||
</> |
||||
) |
||||
} |
||||
|
||||
function SidebarAccountMenu({ |
||||
onSwitchAccount, |
||||
onLogoutClick |
||||
}: { |
||||
onSwitchAccount: () => void |
||||
onLogoutClick: () => void |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const { account, profile } = useNostr() |
||||
const { current, display } = usePrimaryPage() |
||||
const pubkey = account?.pubkey |
||||
const active = useMemo(() => current === 'profile' && display, [display, current]) |
||||
|
||||
if (!pubkey) return null |
||||
|
||||
const defaultAvatar = generateImageByPubkey(pubkey) |
||||
const npub = pubkeyToNpub(pubkey) |
||||
const fallbackUsername = npub ? formatNpub(npub) : formatPubkey(pubkey) |
||||
const { username, avatar } = profile || { username: fallbackUsername, avatar: defaultAvatar } |
||||
|
||||
return ( |
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button |
||||
type="button" |
||||
variant="ghost" |
||||
title={t('Account menu')} |
||||
aria-label={t('Account menu')} |
||||
className={cn( |
||||
'clickable h-12 min-w-0 justify-start gap-2 rounded-lg bg-transparent p-2 text-lg font-semibold text-foreground shadow-none hover:text-accent-foreground', |
||||
'w-12 xl:w-full xl:px-2 xl:py-2', |
||||
active && 'bg-accent/50' |
||||
)} |
||||
> |
||||
<Avatar className="size-8 shrink-0"> |
||||
<AvatarImage src={avatar} /> |
||||
<AvatarFallback> |
||||
<img src={defaultAvatar} alt="" /> |
||||
</AvatarFallback> |
||||
</Avatar> |
||||
<span className="truncate max-xl:hidden">{username}</span> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent side="top" align="end" className="z-[220]"> |
||||
<AccountDropdownItems onSwitchAccount={onSwitchAccount} onLogoutClick={onLogoutClick} /> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
) |
||||
} |
||||
|
||||
function TitlebarAccountMenu({ |
||||
onSwitchAccount, |
||||
onLogoutClick |
||||
}: { |
||||
onSwitchAccount: () => void |
||||
onLogoutClick: () => void |
||||
}) { |
||||
const { t } = useTranslation() |
||||
const { profile } = useNostr() |
||||
const { current, display } = usePrimaryPage() |
||||
const defaultAvatar = useMemo( |
||||
() => (profile?.pubkey ? generateImageByPubkey(profile.pubkey) : ''), |
||||
[profile] |
||||
) |
||||
const active = useMemo(() => current === 'profile' && display, [display, current]) |
||||
|
||||
return ( |
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button |
||||
variant="ghost" |
||||
size="titlebar-icon" |
||||
className={cn(active ? 'bg-accent/50' : '')} |
||||
title={t('Account menu')} |
||||
aria-label={t('Account menu')} |
||||
> |
||||
{profile ? ( |
||||
<Avatar className={cn('w-6 h-6', active ? 'ring-primary ring-1' : '')}> |
||||
<AvatarImage src={profile.avatar} className="object-cover object-center" /> |
||||
<AvatarFallback> |
||||
<img src={defaultAvatar} alt="" /> |
||||
</AvatarFallback> |
||||
</Avatar> |
||||
) : ( |
||||
<Skeleton className={cn('w-6 h-6 rounded-full', active ? 'ring-primary ring-1' : '')} /> |
||||
)} |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent align="end" side="bottom" className="z-[220]"> |
||||
<AccountDropdownItems onSwitchAccount={onSwitchAccount} onLogoutClick={onLogoutClick} /> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
) |
||||
} |
||||
|
||||
/** |
||||
* Help (?) + account avatar with the same dropdown on sidebar (desktop) and titlebar (mobile). |
||||
*/ |
||||
export default function HelpAndAccountMenu({ variant }: { variant: HelpAndAccountMenuVariant }) { |
||||
const { t } = useTranslation() |
||||
const { pubkey, checkLogin } = useNostr() |
||||
const [loginDialogOpen, setLoginDialogOpen] = useState(false) |
||||
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false) |
||||
|
||||
const help = |
||||
variant === 'sidebar' ? <KeyboardShortcutsHelpSidebarButton /> : <KeyboardShortcutsHelpButton /> |
||||
|
||||
let account: ReactNode |
||||
if (pubkey) { |
||||
account = |
||||
variant === 'sidebar' ? ( |
||||
<SidebarAccountMenu |
||||
onSwitchAccount={() => setLoginDialogOpen(true)} |
||||
onLogoutClick={() => setLogoutDialogOpen(true)} |
||||
/> |
||||
) : ( |
||||
<TitlebarAccountMenu |
||||
onSwitchAccount={() => setLoginDialogOpen(true)} |
||||
onLogoutClick={() => setLogoutDialogOpen(true)} |
||||
/> |
||||
) |
||||
} else if (variant === 'sidebar') { |
||||
account = ( |
||||
<SidebarItem onClick={() => checkLogin()} title="Login"> |
||||
<LogIn strokeWidth={3} /> |
||||
</SidebarItem> |
||||
) |
||||
} else { |
||||
account = ( |
||||
<Button variant="ghost" size="titlebar-icon" onClick={() => checkLogin()} title={t('Login')}> |
||||
<UserRound /> |
||||
</Button> |
||||
) |
||||
} |
||||
|
||||
const wrapClass = |
||||
variant === 'titlebar' ? 'flex shrink-0 items-center gap-1' : 'flex flex-col space-y-2' |
||||
|
||||
return ( |
||||
<> |
||||
<div className={wrapClass}> |
||||
{help} |
||||
{account} |
||||
</div> |
||||
<LoginDialog open={loginDialogOpen} setOpen={setLoginDialogOpen} /> |
||||
<LogoutDialog open={logoutDialogOpen} setOpen={setLogoutDialogOpen} /> |
||||
</> |
||||
) |
||||
} |
||||
@ -1,107 +0,0 @@
@@ -1,107 +0,0 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' |
||||
import { Button } from '@/components/ui/button' |
||||
import { |
||||
DropdownMenu, |
||||
DropdownMenuContent, |
||||
DropdownMenuItem, |
||||
DropdownMenuSeparator, |
||||
DropdownMenuTrigger |
||||
} from '@/components/ui/dropdown-menu' |
||||
import { formatPubkey, generateImageByPubkey, pubkeyToNpub, formatNpub } from '@/lib/pubkey' |
||||
import { usePrimaryPage } from '@/PageManager' |
||||
import { useNostr } from '@/providers/NostrProvider' |
||||
import { ArrowDownUp, LogIn, LogOut, MoreVertical, Settings } from 'lucide-react' |
||||
import { useState } from 'react' |
||||
import { useTranslation } from 'react-i18next' |
||||
import LoginDialog from '../LoginDialog' |
||||
import LogoutDialog from '../LogoutDialog' |
||||
import SidebarItem from './SidebarItem' |
||||
|
||||
export default function AccountButton() { |
||||
const { pubkey } = useNostr() |
||||
|
||||
if (pubkey) { |
||||
return <ProfileButton /> |
||||
} else { |
||||
return <LoginButton /> |
||||
} |
||||
} |
||||
|
||||
function ProfileButton() { |
||||
const { t } = useTranslation() |
||||
const { account, profile } = useNostr() |
||||
const pubkey = account?.pubkey |
||||
const { navigate } = usePrimaryPage() |
||||
const [loginDialogOpen, setLoginDialogOpen] = useState(false) |
||||
const [logoutDialogOpen, setLogoutDialogOpen] = useState(false) |
||||
if (!pubkey) return null |
||||
|
||||
const defaultAvatar = generateImageByPubkey(pubkey) |
||||
const npub = pubkeyToNpub(pubkey) |
||||
const fallbackUsername = npub ? formatNpub(npub) : formatPubkey(pubkey) |
||||
const { username, avatar } = profile || { username: fallbackUsername, avatar: defaultAvatar } |
||||
|
||||
return ( |
||||
<div className="flex w-full min-w-0 items-center gap-0.5 xl:gap-1"> |
||||
<Button |
||||
type="button" |
||||
variant="ghost" |
||||
title={t('Profile')} |
||||
className="clickable h-12 min-w-0 flex-1 justify-start gap-2 rounded-lg bg-transparent p-2 text-lg font-semibold text-foreground shadow-none hover:text-accent-foreground xl:px-2 xl:py-2" |
||||
onClick={() => navigate('profile')} |
||||
> |
||||
<Avatar className="size-8 shrink-0"> |
||||
<AvatarImage src={avatar} /> |
||||
<AvatarFallback> |
||||
<img src={defaultAvatar} alt="" /> |
||||
</AvatarFallback> |
||||
</Avatar> |
||||
<span className="truncate max-xl:hidden">{username}</span> |
||||
</Button> |
||||
<DropdownMenu> |
||||
<DropdownMenuTrigger asChild> |
||||
<Button |
||||
type="button" |
||||
variant="ghost" |
||||
size="icon" |
||||
className="size-10 shrink-0 rounded-lg" |
||||
title={t('Account menu')} |
||||
aria-label={t('Account menu')} |
||||
> |
||||
<MoreVertical className="size-5" /> |
||||
</Button> |
||||
</DropdownMenuTrigger> |
||||
<DropdownMenuContent side="top" align="end"> |
||||
<DropdownMenuItem onClick={() => navigate('settings')}> |
||||
<Settings /> |
||||
{t('Settings')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuSeparator /> |
||||
<DropdownMenuItem onClick={() => setLoginDialogOpen(true)}> |
||||
<ArrowDownUp /> |
||||
{t('Switch account')} |
||||
</DropdownMenuItem> |
||||
<DropdownMenuItem |
||||
className="text-destructive focus:text-destructive" |
||||
onClick={() => setLogoutDialogOpen(true)} |
||||
> |
||||
<LogOut /> |
||||
{t('Logout')} |
||||
</DropdownMenuItem> |
||||
</DropdownMenuContent> |
||||
</DropdownMenu> |
||||
<LoginDialog open={loginDialogOpen} setOpen={setLoginDialogOpen} /> |
||||
<LogoutDialog open={logoutDialogOpen} setOpen={setLogoutDialogOpen} /> |
||||
</div> |
||||
) |
||||
} |
||||
|
||||
function LoginButton() { |
||||
const { checkLogin } = useNostr() |
||||
|
||||
return ( |
||||
<SidebarItem onClick={() => checkLogin()} title="Login"> |
||||
<LogIn strokeWidth={3} /> |
||||
</SidebarItem> |
||||
) |
||||
} |
||||
@ -1,43 +0,0 @@
@@ -1,43 +0,0 @@
|
||||
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' |
||||
import { Button } from '@/components/ui/button' |
||||
import { Skeleton } from '@/components/ui/skeleton' |
||||
import { generateImageByPubkey } from '@/lib/pubkey' |
||||
import { cn } from '@/lib/utils' |
||||
import { usePrimaryPage } from '@/PageManager' |
||||
import { useNostr } from '@/providers/NostrProvider' |
||||
import { UserRound } from 'lucide-react' |
||||
import { useMemo } from 'react' |
||||
|
||||
export default function AccountButton() { |
||||
const { navigate, current, display } = usePrimaryPage() |
||||
const { pubkey, profile } = useNostr() |
||||
const defaultAvatar = useMemo( |
||||
() => (profile?.pubkey ? generateImageByPubkey(profile.pubkey) : ''), |
||||
[profile] |
||||
) |
||||
const active = useMemo(() => current === 'profile' && display, [display, current]) |
||||
|
||||
return ( |
||||
<Button |
||||
variant="ghost" |
||||
size="titlebar-icon" |
||||
onClick={() => navigate(pubkey ? 'profile' : 'me')} |
||||
className={active ? 'bg-accent/50' : ''} |
||||
> |
||||
{pubkey ? ( |
||||
profile ? ( |
||||
<Avatar className={cn('w-6 h-6', active ? 'ring-primary ring-1' : '')}> |
||||
<AvatarImage src={profile.avatar} className="object-cover object-center" /> |
||||
<AvatarFallback> |
||||
<img src={defaultAvatar} /> |
||||
</AvatarFallback> |
||||
</Avatar> |
||||
) : ( |
||||
<Skeleton className={cn('w-6 h-6 rounded-full', active ? 'ring-primary ring-1' : '')} /> |
||||
) |
||||
) : ( |
||||
<UserRound /> |
||||
)} |
||||
</Button> |
||||
) |
||||
} |
||||
Loading…
Reference in new issue