refactor: sidebar

This commit is contained in:
codytseng
2025-10-17 23:34:56 +08:00
parent 21663711f8
commit 057de9595b
13 changed files with 110 additions and 44 deletions

View File

@@ -9,6 +9,7 @@ import {
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { toWallet } from '@/lib/link' import { toWallet } from '@/lib/link'
import { formatPubkey, generateImageByPubkey } from '@/lib/pubkey' import { formatPubkey, generateImageByPubkey } from '@/lib/pubkey'
import { cn } from '@/lib/utils'
import { usePrimaryPage, useSecondaryPage } from '@/PageManager' import { usePrimaryPage, useSecondaryPage } from '@/PageManager'
import { useNostr } from '@/providers/NostrProvider' import { useNostr } from '@/providers/NostrProvider'
import { ArrowDownUp, LogIn, LogOut, UserRound, Wallet } from 'lucide-react' import { ArrowDownUp, LogIn, LogOut, UserRound, Wallet } from 'lucide-react'
@@ -18,17 +19,17 @@ import LoginDialog from '../LoginDialog'
import LogoutDialog from '../LogoutDialog' import LogoutDialog from '../LogoutDialog'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'
export default function AccountButton() { export default function AccountButton({ collapse }: { collapse: boolean }) {
const { pubkey } = useNostr() const { pubkey } = useNostr()
if (pubkey) { if (pubkey) {
return <ProfileButton /> return <ProfileButton collapse={collapse} />
} else { } else {
return <LoginButton /> return <LoginButton collapse={collapse} />
} }
} }
function ProfileButton() { function ProfileButton({ collapse }: { collapse: boolean }) {
const { t } = useTranslation() const { t } = useTranslation()
const { account, profile } = useNostr() const { account, profile } = useNostr()
const pubkey = account?.pubkey const pubkey = account?.pubkey
@@ -46,7 +47,10 @@ function ProfileButton() {
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
variant="ghost" variant="ghost"
className="clickable shadow-none p-2 xl:px-2 xl:py-2 w-12 h-12 xl:w-full xl:h-auto flex items-center bg-transparent text-foreground hover:text-accent-foreground rounded-lg justify-start gap-4 text-lg font-semibold" className={cn(
'clickable shadow-none p-2 flex items-center bg-transparent text-foreground hover:text-accent-foreground rounded-lg justify-start gap-4 text-lg font-semibold',
collapse ? 'w-12 h-12' : 'w-full h-auto'
)}
> >
<div className="flex gap-2 items-center flex-1 w-0"> <div className="flex gap-2 items-center flex-1 w-0">
<Avatar className="w-8 h-8"> <Avatar className="w-8 h-8">
@@ -55,7 +59,7 @@ function ProfileButton() {
<img src={defaultAvatar} /> <img src={defaultAvatar} />
</AvatarFallback> </AvatarFallback>
</Avatar> </Avatar>
<div className="truncate font-semibold text-lg">{username}</div> {!collapse && <div className="truncate font-semibold text-lg">{username}</div>}
</div> </div>
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -88,12 +92,12 @@ function ProfileButton() {
) )
} }
function LoginButton() { function LoginButton({ collapse }: { collapse: boolean }) {
const { checkLogin } = useNostr() const { checkLogin } = useNostr()
return ( return (
<SidebarItem onClick={() => checkLogin()} title="Login"> <SidebarItem onClick={() => checkLogin()} title="Login" collapse={collapse}>
<LogIn strokeWidth={3} /> <LogIn />
</SidebarItem> </SidebarItem>
) )
} }

View File

@@ -3,7 +3,7 @@ import { Compass } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'
export default function RelaysButton() { export default function RelaysButton({ collapse }: { collapse: boolean }) {
const { t } = useTranslation() const { t } = useTranslation()
const { navigate, current } = usePrimaryPage() const { navigate, current } = usePrimaryPage()
@@ -12,8 +12,9 @@ export default function RelaysButton() {
title={t('Explore')} title={t('Explore')}
onClick={() => navigate('explore')} onClick={() => navigate('explore')}
active={current === 'explore'} active={current === 'explore'}
collapse={collapse}
> >
<Compass strokeWidth={3} /> <Compass />
</SidebarItem> </SidebarItem>
) )
} }

View File

@@ -2,12 +2,17 @@ import { usePrimaryPage } from '@/PageManager'
import { Home } from 'lucide-react' import { Home } from 'lucide-react'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'
export default function HomeButton() { export default function HomeButton({ collapse }: { collapse: boolean }) {
const { navigate, current } = usePrimaryPage() const { navigate, current } = usePrimaryPage()
return ( return (
<SidebarItem title="Home" onClick={() => navigate('home')} active={current === 'home'}> <SidebarItem
<Home strokeWidth={3} /> title="Home"
onClick={() => navigate('home')}
active={current === 'home'}
collapse={collapse}
>
<Home />
</SidebarItem> </SidebarItem>
) )
} }

View File

@@ -4,7 +4,7 @@ import { useNotification } from '@/providers/NotificationProvider'
import { Bell } from 'lucide-react' import { Bell } from 'lucide-react'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'
export default function NotificationsButton() { export default function NotificationsButton({ collapse }: { collapse: boolean }) {
const { checkLogin } = useNostr() const { checkLogin } = useNostr()
const { navigate, current } = usePrimaryPage() const { navigate, current } = usePrimaryPage()
const { hasNewNotification } = useNotification() const { hasNewNotification } = useNotification()
@@ -14,9 +14,10 @@ export default function NotificationsButton() {
title="Notifications" title="Notifications"
onClick={() => checkLogin(() => navigate('notifications'))} onClick={() => checkLogin(() => navigate('notifications'))}
active={current === 'notifications'} active={current === 'notifications'}
collapse={collapse}
> >
<div className="relative"> <div className="relative">
<Bell strokeWidth={3} /> <Bell />
{hasNewNotification && ( {hasNewNotification && (
<div className="absolute -top-1 right-0 w-2 h-2 ring-2 ring-background bg-primary rounded-full" /> <div className="absolute -top-1 right-0 w-2 h-2 ring-2 ring-background bg-primary rounded-full" />
)} )}

View File

@@ -1,10 +1,11 @@
import PostEditor from '@/components/PostEditor' import PostEditor from '@/components/PostEditor'
import { cn } from '@/lib/utils'
import { useNostr } from '@/providers/NostrProvider' 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({ collapse }: { collapse: boolean }) {
const { checkLogin } = useNostr() const { checkLogin } = useNostr()
const [open, setOpen] = useState(false) const [open, setOpen] = useState(false)
@@ -20,9 +21,10 @@ export default function PostButton() {
}) })
}} }}
variant="default" variant="default"
className="bg-primary xl:justify-center gap-2" className={cn('bg-primary gap-2', !collapse && 'justify-center')}
collapse={collapse}
> >
<PencilLine strokeWidth={3} /> <PencilLine />
</SidebarItem> </SidebarItem>
<PostEditor open={open} setOpen={setOpen} /> <PostEditor open={open} setOpen={setOpen} />
</div> </div>

View File

@@ -3,7 +3,7 @@ import { useNostr } from '@/providers/NostrProvider'
import { UserRound } from 'lucide-react' import { UserRound } from 'lucide-react'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'
export default function ProfileButton() { export default function ProfileButton({ collapse }: { collapse: boolean }) {
const { navigate, current } = usePrimaryPage() const { navigate, current } = usePrimaryPage()
const { checkLogin } = useNostr() const { checkLogin } = useNostr()
@@ -12,8 +12,9 @@ export default function ProfileButton() {
title="Profile" title="Profile"
onClick={() => checkLogin(() => navigate('profile'))} onClick={() => checkLogin(() => navigate('profile'))}
active={current === 'profile'} active={current === 'profile'}
collapse={collapse}
> >
<UserRound strokeWidth={3} /> <UserRound />
</SidebarItem> </SidebarItem>
) )
} }

View File

@@ -2,7 +2,7 @@ import { usePrimaryPage } from '@/PageManager'
import { Search } from 'lucide-react' import { Search } from 'lucide-react'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'
export default function SearchButton() { export default function SearchButton({ collapse }: { collapse: boolean }) {
const { navigate, current, display } = usePrimaryPage() const { navigate, current, display } = usePrimaryPage()
return ( return (
@@ -10,8 +10,9 @@ export default function SearchButton() {
title="Search" title="Search"
onClick={() => navigate('search')} onClick={() => navigate('search')}
active={current === 'search' && display} active={current === 'search' && display}
collapse={collapse}
> >
<Search strokeWidth={3} /> <Search />
</SidebarItem> </SidebarItem>
) )
} }

View File

@@ -3,12 +3,12 @@ import { useSecondaryPage } from '@/PageManager'
import { Settings } from 'lucide-react' import { Settings } from 'lucide-react'
import SidebarItem from './SidebarItem' import SidebarItem from './SidebarItem'
export default function SettingsButton() { export default function SettingsButton({ collapse }: { collapse: boolean }) {
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
return ( return (
<SidebarItem title="Settings" onClick={() => push(toSettings())}> <SidebarItem title="Settings" onClick={() => push(toSettings())} collapse={collapse}>
<Settings strokeWidth={3} /> <Settings />
</SidebarItem> </SidebarItem>
) )
} }

View File

@@ -5,14 +5,17 @@ import { useTranslation } from 'react-i18next'
const SidebarItem = forwardRef< const SidebarItem = forwardRef<
HTMLButtonElement, HTMLButtonElement,
ButtonProps & { title: string; description?: string; active?: boolean } ButtonProps & { title: string; collapse: boolean; description?: string; active?: boolean }
>(({ children, title, description, className, active, ...props }, ref) => { >(({ children, title, description, className, active, collapse, ...props }, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
return ( return (
<Button <Button
className={cn( className={cn(
'flex shadow-none items-center transition-colors duration-500 bg-transparent w-12 h-12 xl:w-full xl:h-auto p-3 m-0 xl:py-2 xl:px-3 rounded-lg xl:justify-start gap-4 text-lg font-semibold [&_svg]:size-full xl:[&_svg]:size-4', 'flex shadow-none items-center transition-colors duration-500 bg-transparent m-0 rounded-lg gap-4 text-lg font-semibold',
collapse
? 'w-12 h-12 p-3 [&_svg]:size-full'
: 'justify-start w-full h-auto py-2 px-3 [&_svg]:size-5',
active && 'text-primary hover:text-primary bg-primary/10 hover:bg-primary/10', active && 'text-primary hover:text-primary bg-primary/10 hover:bg-primary/10',
className className
)} )}
@@ -22,7 +25,7 @@ const SidebarItem = forwardRef<
{...props} {...props}
> >
{children} {children}
<div className="max-xl:hidden">{t(description ?? title)}</div> {!collapse && <div>{t(description ?? title)}</div>}
</Button> </Button>
) )
}) })

View File

@@ -1,6 +1,9 @@
import Icon from '@/assets/Icon' import Icon from '@/assets/Icon'
import Logo from '@/assets/Logo' import Logo from '@/assets/Logo'
import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import { ChevronsLeft, ChevronsRight } from 'lucide-react'
import AccountButton from './AccountButton' import AccountButton from './AccountButton'
import RelaysButton from './ExploreButton' import RelaysButton from './ExploreButton'
import HomeButton from './HomeButton' import HomeButton from './HomeButton'
@@ -12,24 +15,45 @@ import SettingsButton from './SettingsButton'
export default function PrimaryPageSidebar() { export default function PrimaryPageSidebar() {
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const { sidebarCollapse, updateSidebarCollapse } = useUserPreferences()
if (isSmallScreen) return null if (isSmallScreen) return null
return ( return (
<div className="w-16 xl:w-52 flex flex-col pb-2 pt-4 px-2 xl:px-4 justify-between h-full shrink-0"> <div
className={cn(
'relative flex flex-col pb-2 pt-3 justify-between h-full shrink-0',
sidebarCollapse ? 'px-2 w-16' : 'px-4 w-52'
)}
>
<div className="space-y-2"> <div className="space-y-2">
<div className="px-3 xl:px-4 mb-6 w-full"> {sidebarCollapse ? (
<Icon className="xl:hidden" /> <div className="px-3 py-1 mb-6 w-full">
<Logo className="max-xl:hidden" /> <Icon />
</div> </div>
<HomeButton /> ) : (
<RelaysButton /> <div className="px-4 mb-6 w-full">
<NotificationsButton /> <Logo />
<SearchButton />
<ProfileButton />
<SettingsButton />
<PostButton />
</div> </div>
<AccountButton /> )}
<HomeButton collapse={sidebarCollapse} />
<RelaysButton collapse={sidebarCollapse} />
<NotificationsButton collapse={sidebarCollapse} />
<SearchButton collapse={sidebarCollapse} />
<ProfileButton collapse={sidebarCollapse} />
<SettingsButton collapse={sidebarCollapse} />
<PostButton collapse={sidebarCollapse} />
</div>
<AccountButton collapse={sidebarCollapse} />
<button
className="absolute flex flex-col justify-center items-center top-5 right-0 w-5 h-6 p-0 rounded-l-md hover:shadow-md text-muted-foreground hover:text-foreground hover:bg-background transition-colors [&_svg]:size-4"
onClick={(e) => {
e.stopPropagation()
updateSidebarCollapse(!sidebarCollapse)
}}
>
{sidebarCollapse ? <ChevronsRight /> : <ChevronsLeft />}
</button>
</div> </div>
) )
} }

View File

@@ -48,6 +48,7 @@ export const StorageKey = {
NOTIFICATION_LIST_STYLE: 'notificationListStyle', NOTIFICATION_LIST_STYLE: 'notificationListStyle',
MEDIA_AUTO_LOAD_POLICY: 'mediaAutoLoadPolicy', MEDIA_AUTO_LOAD_POLICY: 'mediaAutoLoadPolicy',
SHOWN_CREATE_WALLET_GUIDE_TOAST_PUBKEYS: 'shownCreateWalletGuideToastPubkeys', SHOWN_CREATE_WALLET_GUIDE_TOAST_PUBKEYS: 'shownCreateWalletGuideToastPubkeys',
SIDEBAR_COLLAPSE: 'sidebarCollapse',
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated

View File

@@ -8,6 +8,9 @@ type TUserPreferencesContext = {
muteMedia: boolean muteMedia: boolean
updateMuteMedia: (mute: boolean) => void updateMuteMedia: (mute: boolean) => void
sidebarCollapse: boolean
updateSidebarCollapse: (collapse: boolean) => void
} }
const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined) const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined)
@@ -25,19 +28,27 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
storage.getNotificationListStyle() storage.getNotificationListStyle()
) )
const [muteMedia, setMuteMedia] = useState(true) const [muteMedia, setMuteMedia] = useState(true)
const [sidebarCollapse, setSidebarCollapse] = useState(storage.getSidebarCollapse())
const updateNotificationListStyle = (style: TNotificationStyle) => { const updateNotificationListStyle = (style: TNotificationStyle) => {
setNotificationListStyle(style) setNotificationListStyle(style)
storage.setNotificationListStyle(style) storage.setNotificationListStyle(style)
} }
const updateSidebarCollapse = (collapse: boolean) => {
setSidebarCollapse(collapse)
storage.setSidebarCollapse(collapse)
}
return ( return (
<UserPreferencesContext.Provider <UserPreferencesContext.Provider
value={{ value={{
notificationListStyle, notificationListStyle,
updateNotificationListStyle, updateNotificationListStyle,
muteMedia, muteMedia,
updateMuteMedia: setMuteMedia updateMuteMedia: setMuteMedia,
sidebarCollapse,
updateSidebarCollapse: updateSidebarCollapse
}} }}
> >
{children} {children}

View File

@@ -48,6 +48,7 @@ class LocalStorageService {
private notificationListStyle: TNotificationStyle = NOTIFICATION_LIST_STYLE.DETAILED private notificationListStyle: TNotificationStyle = NOTIFICATION_LIST_STYLE.DETAILED
private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS
private shownCreateWalletGuideToastPubkeys: Set<string> = new Set() private shownCreateWalletGuideToastPubkeys: Set<string> = new Set()
private sidebarCollapse: boolean = false
constructor() { constructor() {
if (!LocalStorageService.instance) { if (!LocalStorageService.instance) {
@@ -193,6 +194,8 @@ class LocalStorageService {
? new Set(JSON.parse(shownCreateWalletGuideToastPubkeysStr)) ? new Set(JSON.parse(shownCreateWalletGuideToastPubkeysStr))
: new Set() : new Set()
this.sidebarCollapse = window.localStorage.getItem(StorageKey.SIDEBAR_COLLAPSE) === 'true'
// Clean up deprecated data // Clean up deprecated data
window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_PROFILE_EVENT_MAP)
window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP) window.localStorage.removeItem(StorageKey.ACCOUNT_FOLLOW_LIST_EVENT_MAP)
@@ -476,6 +479,15 @@ class LocalStorageService {
JSON.stringify(Array.from(this.shownCreateWalletGuideToastPubkeys)) JSON.stringify(Array.from(this.shownCreateWalletGuideToastPubkeys))
) )
} }
getSidebarCollapse() {
return this.sidebarCollapse
}
setSidebarCollapse(collapse: boolean) {
this.sidebarCollapse = collapse
window.localStorage.setItem(StorageKey.SIDEBAR_COLLAPSE, collapse.toString())
}
} }
const instance = new LocalStorageService() const instance = new LocalStorageService()