From 936b15e5c2464bebf7ccb9307d29335b943bf1b0 Mon Sep 17 00:00:00 2001 From: codytseng Date: Sun, 19 Oct 2025 18:41:22 +0800 Subject: [PATCH] feat: improve single-column layout --- src/App.tsx | 16 +- src/PageManager.tsx | 82 +++++---- src/components/NewNotesButton/index.tsx | 4 +- src/components/NoteList/index.tsx | 6 +- src/components/ScrollToTopButton/index.tsx | 35 +++- src/components/Settings/index.tsx | 156 ++++++++++++++++++ src/components/Sidebar/SettingsButton.tsx | 12 +- src/layouts/PrimaryPageLayout/index.tsx | 10 +- src/layouts/SecondaryPageLayout/index.tsx | 8 +- src/pages/primary/SettingsPage/index.tsx | 35 ++++ .../AppearanceSettingsPage/index.tsx | 30 ++-- src/pages/secondary/SettingsPage/index.tsx | 149 +---------------- src/providers/UserPreferencesProvider.tsx | 4 +- 13 files changed, 316 insertions(+), 231 deletions(-) create mode 100644 src/components/Settings/index.tsx create mode 100644 src/pages/primary/SettingsPage/index.tsx diff --git a/src/App.tsx b/src/App.tsx index 503b51ec..b5000fc8 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,10 +24,10 @@ import { PageManager } from './PageManager' export default function App(): JSX.Element { return ( - - - - + + + + @@ -58,9 +58,9 @@ export default function App(): JSX.Element { - - - - + + + + ) } diff --git a/src/PageManager.tsx b/src/PageManager.tsx index e4ebfb62..bcd0b355 100644 --- a/src/PageManager.tsx +++ b/src/PageManager.tsx @@ -27,6 +27,7 @@ import NotificationListPage from './pages/primary/NotificationListPage' import ProfilePage from './pages/primary/ProfilePage' import RelayPage from './pages/primary/RelayPage' import SearchPage from './pages/primary/SearchPage' +import SettingsPage from './pages/primary/SettingsPage' import { NotificationProvider } from './providers/NotificationProvider' import { useScreenSize } from './providers/ScreenSizeProvider' import { useTheme } from './providers/ThemeProvider' @@ -63,7 +64,8 @@ const PRIMARY_PAGE_REF_MAP = { profile: createRef(), relay: createRef(), search: createRef(), - bookmark: createRef() + bookmark: createRef(), + settings: createRef() } const PRIMARY_PAGE_MAP = { @@ -74,7 +76,8 @@ const PRIMARY_PAGE_MAP = { profile: , relay: , search: , - bookmark: + bookmark: , + settings: } const PrimaryPageContext = createContext(undefined) @@ -248,7 +251,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { if (needScrollToTop) { PRIMARY_PAGE_REF_MAP[page].current?.scrollToTop('smooth') } - if (isSmallScreen || enableSingleColumnLayout) { + if (enableSingleColumnLayout) { clearSecondaryPages() } } @@ -358,48 +361,39 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { > -
-
-
-
- -
-
- {!!secondaryStack.length && - secondaryStack.map((item, index) => ( -
- {item.component} -
- ))} - {primaryPages.map(({ name, element, props }) => ( -
- {props ? cloneElement(element as React.ReactElement, props) : element} -
- ))} -
-
-
+
+
+
+
+ {!!secondaryStack.length && + secondaryStack.map((item, index) => ( +
+ {item.component} +
+ ))} + {primaryPages.map(({ name, element, props }) => ( +
+ {props ? cloneElement(element as React.ReactElement, props) : element} +
+ ))} +
+
diff --git a/src/components/NewNotesButton/index.tsx b/src/components/NewNotesButton/index.tsx index e6e11bb2..8638ed36 100644 --- a/src/components/NewNotesButton/index.tsx +++ b/src/components/NewNotesButton/index.tsx @@ -2,6 +2,7 @@ import { Button } from '@/components/ui/button' import { SimpleUserAvatar } from '@/components/UserAvatar' import { cn } from '@/lib/utils' import { useScreenSize } from '@/providers/ScreenSizeProvider' +import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { hasBackgroundAudioAtom } from '@/services/media-manager.service' import { useAtomValue } from 'jotai' import { ArrowUp } from 'lucide-react' @@ -17,6 +18,7 @@ export default function NewNotesButton({ onClick?: () => void }) { const { t } = useTranslation() + const { enableSingleColumnLayout } = useUserPreferences() const { isSmallScreen } = useScreenSize() const hasBackgroundAudio = useAtomValue(hasBackgroundAudioAtom) const pubkeys = useMemo(() => { @@ -36,7 +38,7 @@ export default function NewNotesButton({
- {filteredNewEvents.length > 0 && ( - - )}
{supportTouch ? ( + {filteredNewEvents.length > 0 && ( + + )}
) } diff --git a/src/components/ScrollToTopButton/index.tsx b/src/components/ScrollToTopButton/index.tsx index 15add0e1..8d8e3ec1 100644 --- a/src/components/ScrollToTopButton/index.tsx +++ b/src/components/ScrollToTopButton/index.tsx @@ -2,9 +2,11 @@ import { Button } from '@/components/ui/button' import { cn } from '@/lib/utils' import { useDeepBrowsing } from '@/providers/DeepBrowsingProvider' import { useScreenSize } from '@/providers/ScreenSizeProvider' +import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { hasBackgroundAudioAtom } from '@/services/media-manager.service' import { useAtomValue } from 'jotai' import { ChevronUp } from 'lucide-react' +import { useMemo } from 'react' export default function ScrollToTopButton({ scrollAreaRef, @@ -13,14 +15,37 @@ export default function ScrollToTopButton({ scrollAreaRef?: React.RefObject className?: string }) { - const { isSmallScreen } = useScreenSize() + const { enableSingleColumnLayout } = useUserPreferences() const { deepBrowsing, lastScrollTop } = useDeepBrowsing() + const { isSmallScreen } = useScreenSize() const hasBackgroundAudio = useAtomValue(hasBackgroundAudioAtom) - const visible = !deepBrowsing && lastScrollTop > 800 + const visible = useMemo(() => !deepBrowsing && lastScrollTop > 800, [deepBrowsing, lastScrollTop]) const handleScrollToTop = () => { if (!scrollAreaRef) { - window.scrollTo({ top: 0, behavior: 'smooth' }) + // scroll to top with custom animation + const startPosition = window.pageYOffset || document.documentElement.scrollTop + const duration = 500 + const startTime = performance.now() + + const easeInOutQuad = (t: number) => { + return t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t + } + + const scroll = (currentTime: number) => { + const elapsed = currentTime - startTime + const progress = Math.min(elapsed / duration, 1) + const ease = easeInOutQuad(progress) + + const position = startPosition * (1 - ease) + window.scrollTo(0, position) + + if (progress < 1) { + requestAnimationFrame(scroll) + } + } + + requestAnimationFrame(scroll) return } scrollAreaRef.current?.scrollTo({ top: 0, behavior: 'smooth' }) @@ -29,7 +54,9 @@ export default function ScrollToTopButton({ return (
+ push(toGeneralSettings())}> +
+ +
{t('General')}
+
+ +
+ push(toAppearanceSettings())}> +
+ +
{t('Appearance')}
+
+ +
+ push(toRelaySettings())}> +
+ +
{t('Relays')}
+
+ +
+ {!!pubkey && ( + push(toTranslation())}> +
+ +
{t('Translation')}
+
+ +
+ )} + {!!pubkey && ( + push(toWallet())}> +
+ +
{t('Wallet')}
+
+ +
+ )} + {!!pubkey && ( + push(toPostSettings())}> +
+ +
{t('Post settings')}
+
+ +
+ )} + {!!nsec && ( + { + navigator.clipboard.writeText(nsec) + setCopiedNsec(true) + setTimeout(() => setCopiedNsec(false), 2000) + }} + > +
+ +
{t('Copy private key')} (nsec)
+
+ {copiedNsec ? : } +
+ )} + {!!ncryptsec && ( + { + navigator.clipboard.writeText(ncryptsec) + setCopiedNcryptsec(true) + setTimeout(() => setCopiedNcryptsec(false), 2000) + }} + > +
+ +
{t('Copy private key')} (ncryptsec)
+
+ {copiedNcryptsec ? : } +
+ )} + + +
+ +
{t('About')}
+
+
+
+ v{import.meta.env.APP_VERSION} ({import.meta.env.GIT_COMMIT}) +
+ +
+
+
+
+ +
+
+ ) +} + +const SettingItem = forwardRef>( + ({ children, className, ...props }, ref) => { + return ( +
+ {children} +
+ ) + } +) +SettingItem.displayName = 'SettingItem' diff --git a/src/components/Sidebar/SettingsButton.tsx b/src/components/Sidebar/SettingsButton.tsx index 78ddcdd4..c1f35d92 100644 --- a/src/components/Sidebar/SettingsButton.tsx +++ b/src/components/Sidebar/SettingsButton.tsx @@ -1,13 +1,21 @@ import { toSettings } from '@/lib/link' -import { useSecondaryPage } from '@/PageManager' +import { usePrimaryPage, useSecondaryPage } from '@/PageManager' +import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { Settings } from 'lucide-react' import SidebarItem from './SidebarItem' export default function SettingsButton({ collapse }: { collapse: boolean }) { + const { current, navigate } = usePrimaryPage() const { push } = useSecondaryPage() + const { enableSingleColumnLayout } = useUserPreferences() return ( - push(toSettings())} collapse={collapse}> + (enableSingleColumnLayout ? navigate('settings') : push(toSettings()))} + collapse={collapse} + active={enableSingleColumnLayout ? current === 'settings' : false} + > ) diff --git a/src/layouts/PrimaryPageLayout/index.tsx b/src/layouts/PrimaryPageLayout/index.tsx index 7f63dad8..a1400a59 100644 --- a/src/layouts/PrimaryPageLayout/index.tsx +++ b/src/layouts/PrimaryPageLayout/index.tsx @@ -3,7 +3,7 @@ import { Titlebar } from '@/components/Titlebar' import { ScrollArea } from '@/components/ui/scroll-area' import { TPrimaryPageName, usePrimaryPage } from '@/PageManager' import { DeepBrowsingProvider } from '@/providers/DeepBrowsingProvider' -import { useScreenSize } from '@/providers/ScreenSizeProvider' +import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { forwardRef, useEffect, useImperativeHandle, useRef } from 'react' const PrimaryPageLayout = forwardRef( @@ -26,7 +26,7 @@ const PrimaryPageLayout = forwardRef( const scrollAreaRef = useRef(null) const smallScreenScrollAreaRef = useRef(null) const smallScreenLastScrollTopRef = useRef(0) - const { isSmallScreen } = useScreenSize() + const { enableSingleColumnLayout } = useUserPreferences() const { current, display } = usePrimaryPage() useImperativeHandle( @@ -45,7 +45,7 @@ const PrimaryPageLayout = forwardRef( ) useEffect(() => { - if (!isSmallScreen) return + if (!enableSingleColumnLayout) return const isVisible = () => { return smallScreenScrollAreaRef.current?.checkVisibility @@ -65,9 +65,9 @@ const PrimaryPageLayout = forwardRef( return () => { window.removeEventListener('scroll', handleScroll) } - }, [current, isSmallScreen, display]) + }, [current, enableSingleColumnLayout, display]) - if (isSmallScreen) { + if (enableSingleColumnLayout) { return (
{ const scrollAreaRef = useRef(null) - const { isSmallScreen } = useScreenSize() + const { enableSingleColumnLayout } = useUserPreferences() const { currentIndex } = useSecondaryPage() useImperativeHandle( @@ -52,13 +52,13 @@ const SecondaryPageLayout = forwardRef( ) useEffect(() => { - if (isSmallScreen) { + if (enableSingleColumnLayout) { setTimeout(() => window.scrollTo({ top: 0 }), 10) return } }, []) - if (isSmallScreen) { + if (enableSingleColumnLayout) { return (
{ + const layoutRef = useRef(null) + useImperativeHandle(ref, () => layoutRef.current) + + return ( + } + displayScrollToTopButton + > + + + ) +}) +SettingsPage.displayName = 'SettingsPage' +export default SettingsPage + +function SettingsPageTitlebar() { + const { t } = useTranslation() + + return ( +
+ +
{t('Settings')}
+
+ ) +} diff --git a/src/pages/secondary/AppearanceSettingsPage/index.tsx b/src/pages/secondary/AppearanceSettingsPage/index.tsx index aaee0215..b8efd84b 100644 --- a/src/pages/secondary/AppearanceSettingsPage/index.tsx +++ b/src/pages/secondary/AppearanceSettingsPage/index.tsx @@ -2,6 +2,7 @@ import { Label } from '@/components/ui/label' import { PRIMARY_COLORS, TPrimaryColor } from '@/constants' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import { cn } from '@/lib/utils' +import { useScreenSize } from '@/providers/ScreenSizeProvider' import { useTheme } from '@/providers/ThemeProvider' import { useUserPreferences } from '@/providers/UserPreferencesProvider' import { Columns2, LayoutList, List, Monitor, Moon, PanelLeft, Sun } from 'lucide-react' @@ -27,6 +28,7 @@ const NOTIFICATION_STYLES = [ const AppearanceSettingsPage = forwardRef(({ index }: { index?: number }, ref) => { const { t } = useTranslation() + const { isSmallScreen } = useScreenSize() const { themeSetting, setThemeSetting, primaryColor, setPrimaryColor } = useTheme() const { enableSingleColumnLayout, @@ -52,20 +54,22 @@ const AppearanceSettingsPage = forwardRef(({ index }: { index?: number }, ref) = ))}
-
- -
- {LAYOUTS.map(({ key, label, icon }) => ( - updateEnableSingleColumnLayout(key)} - /> - ))} + {!isSmallScreen && ( +
+ +
+ {LAYOUTS.map(({ key, label, icon }) => ( + updateEnableSingleColumnLayout(key)} + /> + ))} +
-
+ )}
diff --git a/src/pages/secondary/SettingsPage/index.tsx b/src/pages/secondary/SettingsPage/index.tsx index 5fe461c9..0349e97b 100644 --- a/src/pages/secondary/SettingsPage/index.tsx +++ b/src/pages/secondary/SettingsPage/index.tsx @@ -1,159 +1,16 @@ -import AboutInfoDialog from '@/components/AboutInfoDialog' -import Donation from '@/components/Donation' +import Settings from '@/components/Settings' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' -import { - toAppearanceSettings, - toGeneralSettings, - toPostSettings, - toRelaySettings, - toTranslation, - toWallet -} from '@/lib/link' -import { cn } from '@/lib/utils' -import { useSecondaryPage } from '@/PageManager' -import { useNostr } from '@/providers/NostrProvider' -import { - Check, - ChevronRight, - Copy, - Info, - KeyRound, - Languages, - Palette, - PencilLine, - Server, - Settings2, - Wallet -} from 'lucide-react' -import { forwardRef, HTMLProps, useState } from 'react' +import { forwardRef } from 'react' import { useTranslation } from 'react-i18next' const SettingsPage = forwardRef(({ index }: { index?: number }, ref) => { const { t } = useTranslation() - const { pubkey, nsec, ncryptsec } = useNostr() - const { push } = useSecondaryPage() - const [copiedNsec, setCopiedNsec] = useState(false) - const [copiedNcryptsec, setCopiedNcryptsec] = useState(false) return ( - push(toGeneralSettings())}> -
- -
{t('General')}
-
- -
- push(toAppearanceSettings())}> -
- -
{t('Appearance')}
-
- -
- push(toRelaySettings())}> -
- -
{t('Relays')}
-
- -
- {!!pubkey && ( - push(toTranslation())}> -
- -
{t('Translation')}
-
- -
- )} - {!!pubkey && ( - push(toWallet())}> -
- -
{t('Wallet')}
-
- -
- )} - {!!pubkey && ( - push(toPostSettings())}> -
- -
{t('Post settings')}
-
- -
- )} - {!!nsec && ( - { - navigator.clipboard.writeText(nsec) - setCopiedNsec(true) - setTimeout(() => setCopiedNsec(false), 2000) - }} - > -
- -
{t('Copy private key')} (nsec)
-
- {copiedNsec ? : } -
- )} - {!!ncryptsec && ( - { - navigator.clipboard.writeText(ncryptsec) - setCopiedNcryptsec(true) - setTimeout(() => setCopiedNcryptsec(false), 2000) - }} - > -
- -
{t('Copy private key')} (ncryptsec)
-
- {copiedNcryptsec ? : } -
- )} - - -
- -
{t('About')}
-
-
-
- v{import.meta.env.APP_VERSION} ({import.meta.env.GIT_COMMIT}) -
- -
-
-
-
- -
+
) }) SettingsPage.displayName = 'SettingsPage' export default SettingsPage - -const SettingItem = forwardRef>( - ({ children, className, ...props }, ref) => { - return ( -
- {children} -
- ) - } -) -SettingItem.displayName = 'SettingItem' diff --git a/src/providers/UserPreferencesProvider.tsx b/src/providers/UserPreferencesProvider.tsx index 19280297..61214bd4 100644 --- a/src/providers/UserPreferencesProvider.tsx +++ b/src/providers/UserPreferencesProvider.tsx @@ -1,6 +1,7 @@ import storage from '@/services/local-storage.service' import { TNotificationStyle } from '@/types' import { createContext, useContext, useState } from 'react' +import { useScreenSize } from './ScreenSizeProvider' type TUserPreferencesContext = { notificationListStyle: TNotificationStyle @@ -27,6 +28,7 @@ export const useUserPreferences = () => { } export function UserPreferencesProvider({ children }: { children: React.ReactNode }) { + const { isSmallScreen } = useScreenSize() const [notificationListStyle, setNotificationListStyle] = useState( storage.getNotificationListStyle() ) @@ -60,7 +62,7 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod updateMuteMedia: setMuteMedia, sidebarCollapse, updateSidebarCollapse, - enableSingleColumnLayout, + enableSingleColumnLayout: isSmallScreen ? true : enableSingleColumnLayout, updateEnableSingleColumnLayout }} >