Browse Source

Make the right-hand panel closeable.

imwald
Silberengel 5 months ago
parent
commit
9ad5ecb6f8
  1. 179
      src/PageManager.tsx
  2. 6
      src/components/NoteCard/MainNoteCard.tsx
  3. 1
      src/constants.ts
  4. 34
      src/layouts/SecondaryPageLayout/index.tsx
  5. 19
      src/pages/secondary/HomePage/index.tsx
  6. 8
      src/pages/secondary/NotePage/index.tsx
  7. 14
      src/providers/UserPreferencesProvider.tsx
  8. 13
      src/services/local-storage.service.ts

179
src/PageManager.tsx

@ -1,9 +1,13 @@
import Sidebar from '@/components/Sidebar' import Sidebar from '@/components/Sidebar'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { ChevronLeft } from 'lucide-react'
import NoteListPage from '@/pages/primary/NoteListPage' import NoteListPage from '@/pages/primary/NoteListPage'
import HomePage from '@/pages/secondary/HomePage' import HomePage from '@/pages/secondary/HomePage'
import NotePage from '@/pages/secondary/NotePage'
import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider' import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider'
import { NotificationProvider } from '@/providers/NotificationProvider' import { NotificationProvider } from '@/providers/NotificationProvider'
import { UserPreferencesProvider, useUserPreferences } from '@/providers/UserPreferencesProvider'
import { TPageRef } from '@/types' import { TPageRef } from '@/types'
import { import {
cloneElement, cloneElement,
@ -78,6 +82,10 @@ const PrimaryPageContext = createContext<TPrimaryPageContext | undefined>(undefi
const SecondaryPageContext = createContext<TSecondaryPageContext | undefined>(undefined) const SecondaryPageContext = createContext<TSecondaryPageContext | undefined>(undefined)
const PrimaryNoteViewContext = createContext<{
setPrimaryNoteView: (view: ReactNode | null) => void
} | undefined>(undefined)
export function usePrimaryPage() { export function usePrimaryPage() {
const context = useContext(PrimaryPageContext) const context = useContext(PrimaryPageContext)
if (!context) { if (!context) {
@ -94,6 +102,127 @@ export function useSecondaryPage() {
return context return context
} }
export function usePrimaryNoteView() {
const context = useContext(PrimaryNoteViewContext)
if (!context) {
throw new Error('usePrimaryNoteView must be used within a PrimaryNoteViewContext.Provider')
}
return context
}
// Custom hook for intelligent note navigation
export function useSmartNoteNavigation() {
const { hideRecommendedRelaysPanel } = useUserPreferences()
const { push: pushSecondary } = useSecondaryPage()
const { setPrimaryNoteView } = usePrimaryNoteView()
const navigateToNote = (url: string) => {
if (hideRecommendedRelaysPanel) {
// When right panel is hidden, show note in primary area
// Extract note ID from URL (e.g., "/notes/note1..." -> "note1...")
const noteId = url.replace('/notes/', '')
setPrimaryNoteView(<NotePage id={noteId} hideTitlebar={true} />)
} else {
// Normal behavior - use secondary navigation
pushSecondary(url)
}
}
return { navigateToNote }
}
function ConditionalHomePage() {
const { hideRecommendedRelaysPanel } = useUserPreferences()
if (hideRecommendedRelaysPanel) {
return null
}
return <HomePage />
}
function MainContentArea({
primaryPages,
currentPrimaryPage,
secondaryStack,
primaryNoteView,
setPrimaryNoteView
}: {
primaryPages: { name: TPrimaryPageName; element: ReactNode; props?: any }[]
currentPrimaryPage: TPrimaryPageName
secondaryStack: { index: number; component: ReactNode }[]
primaryNoteView: ReactNode | null
setPrimaryNoteView: (view: ReactNode | null) => void
}) {
const { hideRecommendedRelaysPanel } = useUserPreferences()
// If recommended relays panel is hidden, use single column layout
// Otherwise use two-column grid layout
const gridClass = hideRecommendedRelaysPanel ? "grid-cols-1" : "grid-cols-2"
return (
<div className={`grid ${gridClass} gap-2 w-full pr-2 py-2`}>
<div className="rounded-lg shadow-lg bg-background overflow-hidden">
{hideRecommendedRelaysPanel && primaryNoteView ? (
// Show note view with back button when right panel is hidden
<div className="flex flex-col h-full w-full">
<div className="flex gap-1 p-1 items-center justify-between font-semibold border-b">
<div className="flex items-center flex-1 w-0">
<Button
className="flex gap-1 items-center w-fit max-w-full justify-start pl-2 pr-3"
variant="ghost"
size="titlebar-icon"
title="Back to feed"
onClick={() => setPrimaryNoteView(null)}
>
<ChevronLeft />
<div className="truncate text-lg font-semibold">Note</div>
</Button>
</div>
</div>
<div className="flex-1 overflow-auto">
{primaryNoteView}
</div>
</div>
) : (
// Show normal primary pages
primaryPages.map(({ name, element, props }) => (
<div
key={name}
className="flex flex-col h-full w-full"
style={{
display: currentPrimaryPage === name ? 'block' : 'none'
}}
>
{props ? cloneElement(element as React.ReactElement, props) : element}
</div>
))
)}
</div>
{!hideRecommendedRelaysPanel && (
<div className="rounded-lg shadow-lg bg-background overflow-hidden">
{secondaryStack.map((item, index) => (
<div
key={item.index}
className="flex flex-col h-full w-full"
style={{ display: index === secondaryStack.length - 1 ? 'block' : 'none' }}
>
{item.component}
</div>
))}
<div
key="home"
className="w-full"
style={{ display: secondaryStack.length === 0 ? 'block' : 'none' }}
>
<ConditionalHomePage />
</div>
</div>
)}
</div>
)
}
export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
const [currentPrimaryPage, setCurrentPrimaryPage] = useState<TPrimaryPageName>('home') const [currentPrimaryPage, setCurrentPrimaryPage] = useState<TPrimaryPageName>('home')
const [primaryPages, setPrimaryPages] = useState< const [primaryPages, setPrimaryPages] = useState<
@ -105,6 +234,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
} }
]) ])
const [secondaryStack, setSecondaryStack] = useState<TStackItem[]>([]) const [secondaryStack, setSecondaryStack] = useState<TStackItem[]>([])
const [primaryNoteView, setPrimaryNoteView] = useState<ReactNode | null>(null)
const { isSmallScreen } = useScreenSize() const { isSmallScreen } = useScreenSize()
const ignorePopStateRef = useRef(false) const ignorePopStateRef = useRef(false)
@ -310,6 +440,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
> >
<CurrentRelaysProvider> <CurrentRelaysProvider>
<NotificationProvider> <NotificationProvider>
<UserPreferencesProvider>
<PrimaryNoteViewContext.Provider value={{ setPrimaryNoteView }}>
{!!secondaryStack.length && {!!secondaryStack.length &&
secondaryStack.map((item, index) => ( secondaryStack.map((item, index) => (
<div <div
@ -335,6 +467,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
<BottomNavigationBar /> <BottomNavigationBar />
<TooManyRelaysAlertDialog /> <TooManyRelaysAlertDialog />
<CreateWalletGuideToast /> <CreateWalletGuideToast />
</PrimaryNoteViewContext.Provider>
</UserPreferencesProvider>
</NotificationProvider> </NotificationProvider>
</CurrentRelaysProvider> </CurrentRelaysProvider>
</SecondaryPageContext.Provider> </SecondaryPageContext.Provider>
@ -359,6 +493,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
> >
<CurrentRelaysProvider> <CurrentRelaysProvider>
<NotificationProvider> <NotificationProvider>
<UserPreferencesProvider>
<PrimaryNoteViewContext.Provider value={{ setPrimaryNoteView }}>
<div className="flex flex-col items-center bg-surface-background"> <div className="flex flex-col items-center bg-surface-background">
<div <div
className="flex h-[var(--vh)] w-full bg-surface-background" className="flex h-[var(--vh)] w-full bg-surface-background"
@ -367,43 +503,20 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
}} }}
> >
<Sidebar /> <Sidebar />
<div className="grid grid-cols-2 gap-2 w-full pr-2 py-2"> <MainContentArea
<div className="rounded-lg shadow-lg bg-background overflow-hidden"> primaryPages={primaryPages}
{primaryPages.map(({ name, element, props }) => ( currentPrimaryPage={currentPrimaryPage}
<div secondaryStack={secondaryStack}
key={name} primaryNoteView={primaryNoteView}
className="flex flex-col h-full w-full" setPrimaryNoteView={setPrimaryNoteView}
style={{ />
display: currentPrimaryPage === name ? 'block' : 'none'
}}
>
{props ? cloneElement(element as React.ReactElement, props) : element}
</div>
))}
</div>
<div className="rounded-lg shadow-lg bg-background overflow-hidden">
{secondaryStack.map((item, index) => (
<div
key={item.index}
className="flex flex-col h-full w-full"
style={{ display: index === secondaryStack.length - 1 ? 'block' : 'none' }}
>
{item.component}
</div>
))}
<div
key="home"
className="w-full"
style={{ display: secondaryStack.length === 0 ? 'block' : 'none' }}
>
<HomePage />
</div>
</div>
</div>
</div> </div>
</div> </div>
<BottomNavigationBar />
<TooManyRelaysAlertDialog /> <TooManyRelaysAlertDialog />
<CreateWalletGuideToast /> <CreateWalletGuideToast />
</PrimaryNoteViewContext.Provider>
</UserPreferencesProvider>
</NotificationProvider> </NotificationProvider>
</CurrentRelaysProvider> </CurrentRelaysProvider>
</SecondaryPageContext.Provider> </SecondaryPageContext.Provider>

6
src/components/NoteCard/MainNoteCard.tsx

@ -1,6 +1,6 @@
import { Separator } from '@/components/ui/separator' import { Separator } from '@/components/ui/separator'
import { toNote } from '@/lib/link' import { toNote } from '@/lib/link'
import { useSecondaryPage } from '@/PageManager' import { useSmartNoteNavigation } from '@/PageManager'
import { Event } from 'nostr-tools' import { Event } from 'nostr-tools'
import Collapsible from '../Collapsible' import Collapsible from '../Collapsible'
import Note from '../Note' import Note from '../Note'
@ -20,14 +20,14 @@ export default function MainNoteCard({
embedded?: boolean embedded?: boolean
originalNoteId?: string originalNoteId?: string
}) { }) {
const { push } = useSecondaryPage() const { navigateToNote } = useSmartNoteNavigation()
return ( return (
<div <div
className={className} className={className}
onClick={(e) => { onClick={(e) => {
e.stopPropagation() e.stopPropagation()
push(toNote(originalNoteId ?? event)) navigateToNote(toNote(originalNoteId ?? event))
}} }}
> >
<div className={`clickable ${embedded ? 'p-2 sm:p-3 border rounded-lg' : 'py-3'}`}> <div className={`clickable ${embedded ? 'p-2 sm:p-3 border rounded-lg' : 'py-3'}`}>

1
src/constants.ts

@ -45,6 +45,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',
HIDE_RECOMMENDED_RELAYS_PANEL: 'hideRecommendedRelaysPanel',
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

34
src/layouts/SecondaryPageLayout/index.tsx

@ -66,13 +66,15 @@ const SecondaryPageLayout = forwardRef(
paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)' paddingBottom: 'calc(env(safe-area-inset-bottom) + 3rem)'
}} }}
> >
<SecondaryPageTitlebar {title && (
title={title} <SecondaryPageTitlebar
controls={controls} title={title}
hideBackButton={hideBackButton} controls={controls}
hideBottomBorder={hideTitlebarBottomBorder} hideBackButton={hideBackButton}
titlebar={titlebar} hideBottomBorder={hideTitlebarBottomBorder}
/> titlebar={titlebar}
/>
)}
{children} {children}
</div> </div>
{displayScrollToTopButton && <ScrollToTopButton />} {displayScrollToTopButton && <ScrollToTopButton />}
@ -84,16 +86,18 @@ const SecondaryPageLayout = forwardRef(
<DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}> <DeepBrowsingProvider active={currentIndex === index} scrollAreaRef={scrollAreaRef}>
<ScrollArea <ScrollArea
className="h-[calc(100vh-2rem)] overflow-auto" className="h-[calc(100vh-2rem)] overflow-auto"
scrollBarClassName="z-50 pt-12" scrollBarClassName={title ? "z-50 pt-12" : "z-50"}
ref={scrollAreaRef} ref={scrollAreaRef}
> >
<SecondaryPageTitlebar {title && (
title={title} <SecondaryPageTitlebar
controls={controls} title={title}
hideBackButton={hideBackButton} controls={controls}
hideBottomBorder={hideTitlebarBottomBorder} hideBackButton={hideBackButton}
titlebar={titlebar} hideBottomBorder={hideTitlebarBottomBorder}
/> titlebar={titlebar}
/>
)}
{children} {children}
<div className="h-4" /> <div className="h-4" />
</ScrollArea> </ScrollArea>

19
src/pages/secondary/HomePage/index.tsx

@ -4,9 +4,10 @@ import { Button } from '@/components/ui/button'
import { RECOMMENDED_RELAYS } from '@/constants' import { RECOMMENDED_RELAYS } from '@/constants'
import SecondaryPageLayout from '@/layouts/SecondaryPageLayout' import SecondaryPageLayout from '@/layouts/SecondaryPageLayout'
import { toRelay } from '@/lib/link' import { toRelay } from '@/lib/link'
import { useUserPreferences } from '@/providers/UserPreferencesProvider'
import relayInfoService from '@/services/relay-info.service' import relayInfoService from '@/services/relay-info.service'
import { TRelayInfo } from '@/types' import { TRelayInfo } from '@/types'
import { ArrowRight, Server } from 'lucide-react' import { ArrowRight, Server, X } from 'lucide-react'
import { forwardRef, useEffect, useState } from 'react' import { forwardRef, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -14,6 +15,7 @@ const HomePage = forwardRef(({ index }: { index?: number }, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { navigate } = usePrimaryPage() const { navigate } = usePrimaryPage()
const { push } = useSecondaryPage() const { push } = useSecondaryPage()
const { updateHideRecommendedRelaysPanel } = useUserPreferences()
const [recommendedRelayInfos, setRecommendedRelayInfos] = useState<TRelayInfo[]>([]) const [recommendedRelayInfos, setRecommendedRelayInfos] = useState<TRelayInfo[]>([])
useEffect(() => { useEffect(() => {
@ -43,10 +45,21 @@ const HomePage = forwardRef(({ index }: { index?: number }, ref) => {
ref={ref} ref={ref}
index={index} index={index}
title={ title={
<> <div className="flex items-center gap-2">
<Server /> <Server />
<div>{t('Recommended relays')}</div> <div>{t('Recommended relays')}</div>
</> </div>
}
controls={
<Button
variant="ghost"
size="icon"
className="h-6 w-6"
onClick={() => updateHideRecommendedRelaysPanel(true)}
title={t('Close')}
>
<X className="h-4 w-4" />
</Button>
} }
hideBackButton hideBackButton
hideTitlebarBottomBorder hideTitlebarBottomBorder

8
src/pages/secondary/NotePage/index.tsx

@ -20,7 +20,7 @@ import { forwardRef, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import NotFound from './NotFound' import NotFound from './NotFound'
const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref) => { const NotePage = forwardRef(({ id, index, hideTitlebar = false }: { id?: string; index?: number; hideTitlebar?: boolean }, ref) => {
const { t } = useTranslation() const { t } = useTranslation()
const { event, isFetching } = useFetchEvent(id) const { event, isFetching } = useFetchEvent(id)
const [externalEvent, setExternalEvent] = useState<Event | undefined>(undefined) const [externalEvent, setExternalEvent] = useState<Event | undefined>(undefined)
@ -37,7 +37,7 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref
if (!event && isFetching) { if (!event && isFetching) {
return ( return (
<SecondaryPageLayout ref={ref} index={index} title={t('Note')}> <SecondaryPageLayout ref={ref} index={index} title={hideTitlebar ? undefined : t('Note')}>
<div className="px-4 pt-3"> <div className="px-4 pt-3">
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
<Skeleton className="w-10 h-10 rounded-full" /> <Skeleton className="w-10 h-10 rounded-full" />
@ -64,14 +64,14 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref
} }
if (!finalEvent) { if (!finalEvent) {
return ( return (
<SecondaryPageLayout ref={ref} index={index} title={t('Note')} displayScrollToTopButton> <SecondaryPageLayout ref={ref} index={index} title={hideTitlebar ? undefined : t('Note')} displayScrollToTopButton>
<NotFound bech32Id={id} onEventFound={setExternalEvent} /> <NotFound bech32Id={id} onEventFound={setExternalEvent} />
</SecondaryPageLayout> </SecondaryPageLayout>
) )
} }
return ( return (
<SecondaryPageLayout ref={ref} index={index} title={t('Note')} displayScrollToTopButton> <SecondaryPageLayout ref={ref} index={index} title={hideTitlebar ? undefined : t('Note')} displayScrollToTopButton>
<div className="px-4 pt-3"> <div className="px-4 pt-3">
{rootITag && <ExternalRoot value={rootITag[1]} />} {rootITag && <ExternalRoot value={rootITag[1]} />}
{rootEventId && rootEventId !== parentEventId && ( {rootEventId && rootEventId !== parentEventId && (

14
src/providers/UserPreferencesProvider.tsx

@ -5,6 +5,8 @@ import { createContext, useContext, useState } from 'react'
type TUserPreferencesContext = { type TUserPreferencesContext = {
notificationListStyle: TNotificationStyle notificationListStyle: TNotificationStyle
updateNotificationListStyle: (style: TNotificationStyle) => void updateNotificationListStyle: (style: TNotificationStyle) => void
hideRecommendedRelaysPanel: boolean
updateHideRecommendedRelaysPanel: (hide: boolean) => void
} }
const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined) const UserPreferencesContext = createContext<TUserPreferencesContext | undefined>(undefined)
@ -21,17 +23,27 @@ export function UserPreferencesProvider({ children }: { children: React.ReactNod
const [notificationListStyle, setNotificationListStyle] = useState( const [notificationListStyle, setNotificationListStyle] = useState(
storage.getNotificationListStyle() storage.getNotificationListStyle()
) )
const [hideRecommendedRelaysPanel, setHideRecommendedRelaysPanel] = useState(
storage.getHideRecommendedRelaysPanel()
)
const updateNotificationListStyle = (style: TNotificationStyle) => { const updateNotificationListStyle = (style: TNotificationStyle) => {
setNotificationListStyle(style) setNotificationListStyle(style)
storage.setNotificationListStyle(style) storage.setNotificationListStyle(style)
} }
const updateHideRecommendedRelaysPanel = (hide: boolean) => {
setHideRecommendedRelaysPanel(hide)
storage.setHideRecommendedRelaysPanel(hide)
}
return ( return (
<UserPreferencesContext.Provider <UserPreferencesContext.Provider
value={{ value={{
notificationListStyle, notificationListStyle,
updateNotificationListStyle updateNotificationListStyle,
hideRecommendedRelaysPanel,
updateHideRecommendedRelaysPanel
}} }}
> >
{children} {children}

13
src/services/local-storage.service.ts

@ -49,6 +49,7 @@ class LocalStorageService {
private hideContentMentioningMutedUsers: boolean = false private hideContentMentioningMutedUsers: boolean = false
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 hideRecommendedRelaysPanel: boolean = false
private shownCreateWalletGuideToastPubkeys: Set<string> = new Set() private shownCreateWalletGuideToastPubkeys: Set<string> = new Set()
constructor() { constructor() {
@ -164,6 +165,9 @@ class LocalStorageService {
this.dismissedTooManyRelaysAlert = this.dismissedTooManyRelaysAlert =
window.localStorage.getItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT) === 'true' window.localStorage.getItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT) === 'true'
this.hideRecommendedRelaysPanel =
window.localStorage.getItem(StorageKey.HIDE_RECOMMENDED_RELAYS_PANEL) === 'true'
const showKindsStr = window.localStorage.getItem(StorageKey.SHOW_KINDS) const showKindsStr = window.localStorage.getItem(StorageKey.SHOW_KINDS)
if (!showKindsStr) { if (!showKindsStr) {
// Default: show all supported kinds except reposts // Default: show all supported kinds except reposts
@ -456,6 +460,15 @@ class LocalStorageService {
window.localStorage.setItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT, dismissed.toString()) window.localStorage.setItem(StorageKey.DISMISSED_TOO_MANY_RELAYS_ALERT, dismissed.toString())
} }
getHideRecommendedRelaysPanel() {
return this.hideRecommendedRelaysPanel
}
setHideRecommendedRelaysPanel(hide: boolean) {
this.hideRecommendedRelaysPanel = hide
window.localStorage.setItem(StorageKey.HIDE_RECOMMENDED_RELAYS_PANEL, hide.toString())
}
getShowKinds() { getShowKinds() {
return this.showKinds return this.showKinds
} }

Loading…
Cancel
Save