import React, { useEffect, useRef } from 'react'; import { Category, Feed, FeedEntry, FilterState, Stats } from '../types'; import { Sidebar } from './Sidebar'; import { formatRelativeDate, stripHtml, estimateReadingTime } from '../utils'; import { Star, CheckCircle2, ExternalLink, Share2, Clock, User as UserIcon, CheckCheck, } from 'lucide-react'; interface ThreePaneViewProps { filter: FilterState; onFilterChange: (newFilter: Partial) => void; categories: Category[]; feeds: Feed[]; stats: Stats | null; entries: FeedEntry[]; selectedIndex: number; onSelectEntry: (index: number) => void; onToggleRead: (entry: FeedEntry, e?: React.MouseEvent) => void; onToggleStar: (entry: FeedEntry, e?: React.MouseEvent) => void; onMarkAllRead: () => void; sidebarCollapsed: boolean; } export const ThreePaneView: React.FC = ({ filter, onFilterChange, categories, feeds, stats, entries, selectedIndex, onSelectEntry, onToggleRead, onToggleStar, onMarkAllRead, sidebarCollapsed, }) => { const cardRefs = useRef<(HTMLDivElement | null)[]>([]); const readerPaneRef = useRef(null); const endSentinelRef = useRef(null); const selectedEntry = entries[selectedIndex] || entries[0]; useEffect(() => { if (selectedIndex >= 0 && selectedIndex < entries.length) { const el = cardRefs.current[selectedIndex]; if (el) { el.scrollIntoView({ behavior: 'smooth', block: 'nearest' }); } } }, [selectedIndex, entries.length]); // Robust Auto-Read Detection (Dwell Time for short posts + IntersectionObserver for end of post + Scroll Progress) useEffect(() => { if (!selectedEntry || selectedEntry.is_read) return; let dwellTimer: ReturnType | null = null; // Check if the content fits entirely in the viewport (short article) const checkViewportFit = () => { if (readerPaneRef.current) { const { scrollHeight, clientHeight } = readerPaneRef.current; // If article is short (fits or almost fits), mark as read after 1.5s of reading if (scrollHeight <= clientHeight + 60) { dwellTimer = setTimeout(() => { if (selectedEntry && !selectedEntry.is_read) { onToggleRead(selectedEntry); } }, 1500); } else { // For longer articles, if user stays focused for 5 seconds, also auto-read dwellTimer = setTimeout(() => { if (selectedEntry && !selectedEntry.is_read) { onToggleRead(selectedEntry); } }, 5000); } } }; // Small delay to allow DOM render calculation const timeout = setTimeout(checkViewportFit, 100); // IntersectionObserver to observe bottom of article let observer: IntersectionObserver | null = null; if (endSentinelRef.current && readerPaneRef.current) { observer = new IntersectionObserver( (entriesObs) => { entriesObs.forEach((entryObs) => { if (entryObs.isIntersecting && selectedEntry && !selectedEntry.is_read) { onToggleRead(selectedEntry); } }); }, { root: readerPaneRef.current, threshold: 0.1, } ); observer.observe(endSentinelRef.current); } return () => { clearTimeout(timeout); if (dwellTimer) clearTimeout(dwellTimer); if (observer) observer.disconnect(); }; }, [selectedEntry?.id, selectedEntry?.is_read, onToggleRead]); const handleReaderScroll = (e: React.UIEvent) => { if (!selectedEntry || selectedEntry.is_read) return; const { scrollTop, scrollHeight, clientHeight } = e.currentTarget; const scrollProgress = (scrollTop + clientHeight) / scrollHeight; if (scrollProgress >= 0.85) { onToggleRead(selectedEntry); } }; const getListTitle = () => { if (filter.search) return `Search: "${filter.search}"`; if (filter.feedId) { const f = feeds.find((item) => item.id === filter.feedId); return f ? f.title : 'Feed'; } if (filter.categoryId) { const c = categories.find((item) => item.id === filter.categoryId); return c ? c.title : 'Category'; } if (filter.view === 'starred') return 'Starred'; if (filter.view === 'all') return 'All Articles'; return 'Inbox'; }; return (
{/* Pane 1: Sidebar (Collapsible & Independently Scrollable) */}
{!sidebarCollapsed && ( )}
{/* Pane 2: Article List (Reeder 4 minimal style) */}
{getListTitle()} {entries.length}
{entries.length === 0 ? (
✨

Inbox Zero

No articles to read here.

) : ( entries.map((entry, index) => { const isSelected = index === selectedIndex; const snippet = stripHtml(entry.content || entry.raw_content, 100); const relativeTime = formatRelativeDate(entry.published_at); return (
(cardRefs.current[index] = el)} className={`article-item-card ${isSelected ? 'selected' : ''} ${ entry.is_read ? 'is-read' : '' }`} onClick={() => onSelectEntry(index)} >
{entry.feed_title}
{relativeTime} {!entry.is_read &&
}

{entry.title || 'Untitled'}

{snippet &&

{snippet}

}
); }) )}
{/* Pane 3: Distraction-Free Reading Canvas */}
{selectedEntry ? ( <> {/* Top Toolbar */}
{/* Reading Content */}
{selectedEntry.feed_title} • {' '} {new Date(selectedEntry.published_at).toLocaleDateString(undefined, { month: 'short', day: 'numeric', year: 'numeric', })} • {estimateReadingTime(selectedEntry.content || selectedEntry.raw_content)} {selectedEntry.author && ( <> • {selectedEntry.author} )}

{selectedEntry.title}

{/* End of article sentinel for auto-read observer */}
) : (
📖

No Article Selected

Select an article from the list or press 'j' / 'k' to read.

)}
); };