import React, { useEffect, useRef, useState } from 'react' import { FontAwesomeIcon } from '@fortawesome/react-fontawesome' import { faBookmark, faHighlighter } from '@fortawesome/free-solid-svg-icons' import { RelayPool } from 'applesauce-relay' import { IEventStore } from 'applesauce-core' import { BookmarkList } from './BookmarkList' import ContentPanel from './ContentPanel' import VideoView from './VideoView' import { HighlightsPanel } from './HighlightsPanel' import Settings from './Settings' import Toast from './Toast' import { HighlightButton } from './HighlightButton' import { RelayStatusIndicator } from './RelayStatusIndicator' import { ViewMode } from './Bookmarks' import { Bookmark } from '../types/bookmarks' import { Highlight } from '../types/highlights' import { ReadableContent } from '../services/readerService' import { UserSettings } from '../services/settingsService' import { HighlightVisibility } from './HighlightsPanel' import { HighlightButtonRef } from './HighlightButton' import { BookmarkReference } from '../utils/contentLoader' import { useIsMobile } from '../hooks/useMediaQuery' import { classifyUrl } from '../utils/helpers' import { useScrollDirection } from '../hooks/useScrollDirection' import { IAccount } from 'applesauce-accounts' import { NostrEvent } from 'nostr-tools' interface ThreePaneLayoutProps { // Layout state isCollapsed: boolean isHighlightsCollapsed: boolean isSidebarOpen: boolean showSettings: boolean showExplore?: boolean showMe?: boolean showProfile?: boolean showSupport?: boolean // Bookmarks pane bookmarks: Bookmark[] bookmarksLoading: boolean viewMode: ViewMode isRefreshing: boolean lastFetchTime?: number | null onToggleSidebar: () => void onLogout: () => void onViewModeChange: (mode: ViewMode) => void onOpenSettings: () => void onRefresh: () => void relayPool: RelayPool | null eventStore: IEventStore | null // Content pane readerLoading: boolean readerContent?: ReadableContent selectedUrl?: string settings: UserSettings onSaveSettings: (settings: UserSettings) => Promise onCloseSettings: () => void classifiedHighlights: Highlight[] showHighlights: boolean selectedHighlightId?: string highlightVisibility: HighlightVisibility onHighlightClick: (id: string) => void onTextSelection: (text: string) => void onClearSelection: () => void currentUserPubkey?: string followedPubkeys: Set activeAccount?: IAccount | null currentArticle?: NostrEvent | null // Highlights pane highlights: Highlight[] highlightsLoading: boolean onToggleHighlightsPanel: () => void onSelectUrl: (url: string, bookmark?: BookmarkReference) => void onToggleHighlights: (show: boolean) => void onRefreshHighlights: () => void onHighlightVisibilityChange: (visibility: HighlightVisibility) => void // Highlight button highlightButtonRef: React.RefObject onCreateHighlight: (text: string) => void hasActiveAccount: boolean // Toast toastMessage?: string toastType?: 'success' | 'error' onClearToast: () => void // Optional Explore content explore?: React.ReactNode // Optional Me content me?: React.ReactNode // Optional Profile content profile?: React.ReactNode // Optional Support content support?: React.ReactNode } const ThreePaneLayout: React.FC = (props) => { const isMobile = useIsMobile() const sidebarRef = useRef(null) const highlightsRef = useRef(null) const mainPaneRef = useRef(null) // Detect scroll direction and position to hide/show mobile buttons // Only hide on scroll down when viewing article content const isViewingArticle = !!(props.selectedUrl) const scrollDirection = useScrollDirection({ threshold: 10, enabled: isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && isViewingArticle }) // Track if we're at the top of the page const [isAtTop, setIsAtTop] = useState(true) useEffect(() => { if (!isMobile || !isViewingArticle) return const handleScroll = () => { setIsAtTop(window.scrollY <= 10) } handleScroll() // Check initial position window.addEventListener('scroll', handleScroll, { passive: true }) return () => window.removeEventListener('scroll', handleScroll) }, [isMobile, isViewingArticle]) // Bookmark button: hide only when scrolling down const showBookmarkButton = scrollDirection !== 'down' // Highlights button: hide when scrolling down OR at the top const showHighlightsButton = scrollDirection !== 'down' && !isAtTop // Lock body scroll when mobile sidebar or highlights is open const savedScrollPosition = useRef(0) useEffect(() => { if (isMobile && (props.isSidebarOpen || !props.isHighlightsCollapsed)) { // Save current scroll position savedScrollPosition.current = window.scrollY document.body.style.top = `-${savedScrollPosition.current}px` document.body.classList.add('mobile-sidebar-open') } else { // Restore scroll position document.body.classList.remove('mobile-sidebar-open') document.body.style.top = '' if (savedScrollPosition.current > 0) { // Use requestAnimationFrame to ensure DOM has updated requestAnimationFrame(() => { window.scrollTo(0, savedScrollPosition.current) savedScrollPosition.current = 0 }) } } return () => { document.body.classList.remove('mobile-sidebar-open') document.body.style.top = '' } }, [isMobile, props.isSidebarOpen, props.isHighlightsCollapsed]) // Handle ESC key to close sidebar or highlights useEffect(() => { const { isSidebarOpen, isHighlightsCollapsed, onToggleSidebar, onToggleHighlightsPanel } = props if (!isMobile) return if (!isSidebarOpen && isHighlightsCollapsed) return const handleEscape = (e: KeyboardEvent) => { if (e.key === 'Escape') { if (isSidebarOpen) { onToggleSidebar() } else if (!isHighlightsCollapsed) { onToggleHighlightsPanel() } } } document.addEventListener('keydown', handleEscape) return () => document.removeEventListener('keydown', handleEscape) }, [isMobile, props]) // Trap focus in sidebar when open on mobile useEffect(() => { if (!isMobile || !props.isSidebarOpen || !sidebarRef.current) return const sidebar = sidebarRef.current const focusableElements = sidebar.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ) const firstElement = focusableElements[0] const lastElement = focusableElements[focusableElements.length - 1] const handleTab = (e: KeyboardEvent) => { if (e.key !== 'Tab') return if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault() lastElement?.focus() } } else { if (document.activeElement === lastElement) { e.preventDefault() firstElement?.focus() } } } sidebar.addEventListener('keydown', handleTab) firstElement?.focus() return () => { sidebar.removeEventListener('keydown', handleTab) } }, [isMobile, props.isSidebarOpen]) // Trap focus in highlights panel when open on mobile useEffect(() => { if (!isMobile || props.isHighlightsCollapsed || !highlightsRef.current) return const highlights = highlightsRef.current const focusableElements = highlights.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ) const firstElement = focusableElements[0] const lastElement = focusableElements[focusableElements.length - 1] const handleTab = (e: KeyboardEvent) => { if (e.key !== 'Tab') return if (e.shiftKey) { if (document.activeElement === firstElement) { e.preventDefault() lastElement?.focus() } } else { if (document.activeElement === lastElement) { e.preventDefault() firstElement?.focus() } } } highlights.addEventListener('keydown', handleTab) firstElement?.focus() return () => { highlights.removeEventListener('keydown', handleTab) } }, [isMobile, props.isHighlightsCollapsed]) const handleBackdropClick = () => { if (isMobile) { if (props.isSidebarOpen) { props.onToggleSidebar() } else if (!props.isHighlightsCollapsed) { props.onToggleHighlightsPanel() } } } return ( <> {/* Mobile bookmark button - always show except on settings page */} {isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && ( )} {/* Mobile highlights button - only show when viewing article content */} {isMobile && !props.isSidebarOpen && props.isHighlightsCollapsed && !props.showSettings && isViewingArticle && ( )} {/* Mobile backdrop */} {isMobile && (