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 @@ @@ -1,9 +1,13 @@
import Sidebar from '@/components/Sidebar'
import { Button } from '@/components/ui/button'
import { cn } from '@/lib/utils'
import { ChevronLeft } from 'lucide-react'
import NoteListPage from '@/pages/primary/NoteListPage'
import HomePage from '@/pages/secondary/HomePage'
import NotePage from '@/pages/secondary/NotePage'
import { CurrentRelaysProvider } from '@/providers/CurrentRelaysProvider'
import { NotificationProvider } from '@/providers/NotificationProvider'
import { UserPreferencesProvider, useUserPreferences } from '@/providers/UserPreferencesProvider'
import { TPageRef } from '@/types'
import {
cloneElement,
@ -78,6 +82,10 @@ const PrimaryPageContext = createContext<TPrimaryPageContext | undefined>(undefi @@ -78,6 +82,10 @@ const PrimaryPageContext = createContext<TPrimaryPageContext | undefined>(undefi
const SecondaryPageContext = createContext<TSecondaryPageContext | undefined>(undefined)
const PrimaryNoteViewContext = createContext<{
setPrimaryNoteView: (view: ReactNode | null) => void
} | undefined>(undefined)
export function usePrimaryPage() {
const context = useContext(PrimaryPageContext)
if (!context) {
@ -94,6 +102,127 @@ export function useSecondaryPage() { @@ -94,6 +102,127 @@ export function useSecondaryPage() {
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 }) {
const [currentPrimaryPage, setCurrentPrimaryPage] = useState<TPrimaryPageName>('home')
const [primaryPages, setPrimaryPages] = useState<
@ -105,6 +234,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -105,6 +234,7 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
}
])
const [secondaryStack, setSecondaryStack] = useState<TStackItem[]>([])
const [primaryNoteView, setPrimaryNoteView] = useState<ReactNode | null>(null)
const { isSmallScreen } = useScreenSize()
const ignorePopStateRef = useRef(false)
@ -310,6 +440,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -310,6 +440,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
>
<CurrentRelaysProvider>
<NotificationProvider>
<UserPreferencesProvider>
<PrimaryNoteViewContext.Provider value={{ setPrimaryNoteView }}>
{!!secondaryStack.length &&
secondaryStack.map((item, index) => (
<div
@ -335,6 +467,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -335,6 +467,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
<BottomNavigationBar />
<TooManyRelaysAlertDialog />
<CreateWalletGuideToast />
</PrimaryNoteViewContext.Provider>
</UserPreferencesProvider>
</NotificationProvider>
</CurrentRelaysProvider>
</SecondaryPageContext.Provider>
@ -359,6 +493,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -359,6 +493,8 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
>
<CurrentRelaysProvider>
<NotificationProvider>
<UserPreferencesProvider>
<PrimaryNoteViewContext.Provider value={{ setPrimaryNoteView }}>
<div className="flex flex-col items-center bg-surface-background">
<div
className="flex h-[var(--vh)] w-full bg-surface-background"
@ -367,43 +503,20 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) { @@ -367,43 +503,20 @@ export function PageManager({ maxStackSize = 5 }: { maxStackSize?: number }) {
}}
>
<Sidebar />
<div className="grid grid-cols-2 gap-2 w-full pr-2 py-2">
<div className="rounded-lg shadow-lg bg-background overflow-hidden">
{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>
<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>
<MainContentArea
primaryPages={primaryPages}
currentPrimaryPage={currentPrimaryPage}
secondaryStack={secondaryStack}
primaryNoteView={primaryNoteView}
setPrimaryNoteView={setPrimaryNoteView}
/>
</div>
</div>
<BottomNavigationBar />
<TooManyRelaysAlertDialog />
<CreateWalletGuideToast />
</PrimaryNoteViewContext.Provider>
</UserPreferencesProvider>
</NotificationProvider>
</CurrentRelaysProvider>
</SecondaryPageContext.Provider>

6
src/components/NoteCard/MainNoteCard.tsx

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
import { Separator } from '@/components/ui/separator'
import { toNote } from '@/lib/link'
import { useSecondaryPage } from '@/PageManager'
import { useSmartNoteNavigation } from '@/PageManager'
import { Event } from 'nostr-tools'
import Collapsible from '../Collapsible'
import Note from '../Note'
@ -20,14 +20,14 @@ export default function MainNoteCard({ @@ -20,14 +20,14 @@ export default function MainNoteCard({
embedded?: boolean
originalNoteId?: string
}) {
const { push } = useSecondaryPage()
const { navigateToNote } = useSmartNoteNavigation()
return (
<div
className={className}
onClick={(e) => {
e.stopPropagation()
push(toNote(originalNoteId ?? event))
navigateToNote(toNote(originalNoteId ?? event))
}}
>
<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 = { @@ -45,6 +45,7 @@ export const StorageKey = {
NOTIFICATION_LIST_STYLE: 'notificationListStyle',
MEDIA_AUTO_LOAD_POLICY: 'mediaAutoLoadPolicy',
SHOWN_CREATE_WALLET_GUIDE_TOAST_PUBKEYS: 'shownCreateWalletGuideToastPubkeys',
HIDE_RECOMMENDED_RELAYS_PANEL: 'hideRecommendedRelaysPanel',
MEDIA_UPLOAD_SERVICE: 'mediaUploadService', // deprecated
HIDE_UNTRUSTED_EVENTS: 'hideUntrustedEvents', // deprecated
ACCOUNT_RELAY_LIST_EVENT_MAP: 'accountRelayListEventMap', // deprecated

34
src/layouts/SecondaryPageLayout/index.tsx

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

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

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

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

@ -20,7 +20,7 @@ import { forwardRef, useMemo, useState } from 'react' @@ -20,7 +20,7 @@ import { forwardRef, useMemo, useState } from 'react'
import { useTranslation } from 'react-i18next'
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 { event, isFetching } = useFetchEvent(id)
const [externalEvent, setExternalEvent] = useState<Event | undefined>(undefined)
@ -37,7 +37,7 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref @@ -37,7 +37,7 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref
if (!event && isFetching) {
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="flex items-center space-x-2">
<Skeleton className="w-10 h-10 rounded-full" />
@ -64,14 +64,14 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref @@ -64,14 +64,14 @@ const NotePage = forwardRef(({ id, index }: { id?: string; index?: number }, ref
}
if (!finalEvent) {
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} />
</SecondaryPageLayout>
)
}
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">
{rootITag && <ExternalRoot value={rootITag[1]} />}
{rootEventId && rootEventId !== parentEventId && (

14
src/providers/UserPreferencesProvider.tsx

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

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

@ -49,6 +49,7 @@ class LocalStorageService { @@ -49,6 +49,7 @@ class LocalStorageService {
private hideContentMentioningMutedUsers: boolean = false
private notificationListStyle: TNotificationStyle = NOTIFICATION_LIST_STYLE.DETAILED
private mediaAutoLoadPolicy: TMediaAutoLoadPolicy = MEDIA_AUTO_LOAD_POLICY.ALWAYS
private hideRecommendedRelaysPanel: boolean = false
private shownCreateWalletGuideToastPubkeys: Set<string> = new Set()
constructor() {
@ -164,6 +165,9 @@ class LocalStorageService { @@ -164,6 +165,9 @@ class LocalStorageService {
this.dismissedTooManyRelaysAlert =
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)
if (!showKindsStr) {
// Default: show all supported kinds except reposts
@ -456,6 +460,15 @@ class LocalStorageService { @@ -456,6 +460,15 @@ class LocalStorageService {
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() {
return this.showKinds
}

Loading…
Cancel
Save