diff --git a/src/components/Library/LibraryPublicationGrid.tsx b/src/components/Library/LibraryPublicationGrid.tsx index af1a70ba..e3c67949 100644 --- a/src/components/Library/LibraryPublicationGrid.tsx +++ b/src/components/Library/LibraryPublicationGrid.tsx @@ -1,8 +1,10 @@ import PublicationCard from '@/components/Note/PublicationCard' import { Skeleton } from '@/components/ui/skeleton' +import { libraryPublicationGridColumnClass, usePanelMode } from '@/hooks/usePanelMode' import type { LibraryPublicationEntry } from '@/lib/library-publication-index' import { isBooklistNip32Label } from '@/lib/nip32-label' import { cn } from '@/lib/utils' +import { useScreenSize } from '@/providers/ScreenSizeProvider' import { BookOpen, Highlighter, MessageSquare, Tag } from 'lucide-react' import { useTranslation } from 'react-i18next' @@ -95,10 +97,13 @@ export default function LibraryPublicationGrid({ emptyMessage?: string }) { const { t } = useTranslation() + const { isSmallScreen } = useScreenSize() + const panelMode = usePanelMode() + const gridCols = libraryPublicationGridColumnClass(isSmallScreen, panelMode) if (loading) { return ( -
+
{Array.from({ length: 6 }).map((_, i) => ( ))} @@ -115,7 +120,7 @@ export default function LibraryPublicationGrid({ } return ( -
+
{entries.map((entry) => (
{ + it('uses 1 column on mobile', () => { + expect(libraryPublicationGridColumnClass(true, 'single')).toBe('grid-cols-1') + expect(libraryPublicationGridColumnClass(true, 'double')).toBe('grid-cols-1') + }) + + it('uses 2 columns in double-pane desktop', () => { + expect(libraryPublicationGridColumnClass(false, 'double')).toBe('grid-cols-2') + }) + + it('uses 3 columns in single-pane desktop', () => { + expect(libraryPublicationGridColumnClass(false, 'single')).toBe('grid-cols-3') + }) +}) diff --git a/src/hooks/usePanelMode.ts b/src/hooks/usePanelMode.ts new file mode 100644 index 00000000..e54d2c0f --- /dev/null +++ b/src/hooks/usePanelMode.ts @@ -0,0 +1,26 @@ +import storage from '@/services/local-storage.service' +import { useEffect, useState } from 'react' + +export function usePanelMode(): 'single' | 'double' { + const [panelMode, setPanelMode] = useState<'single' | 'double'>(() => storage.getPanelMode()) + + useEffect(() => { + const onPanelMode = (ev: Event) => { + const mode = (ev as CustomEvent<{ mode: 'single' | 'double' }>).detail?.mode + if (mode === 'single' || mode === 'double') setPanelMode(mode) + } + window.addEventListener('panelModeChanged', onPanelMode) + return () => window.removeEventListener('panelModeChanged', onPanelMode) + }, []) + + return panelMode +} + +export function libraryPublicationGridColumnClass( + isSmallScreen: boolean, + panelMode: 'single' | 'double' +): string { + if (isSmallScreen) return 'grid-cols-1' + if (panelMode === 'double') return 'grid-cols-2' + return 'grid-cols-3' +} diff --git a/src/lib/library-publication-index.test.ts b/src/lib/library-publication-index.test.ts index 30dc9f69..00675a66 100644 --- a/src/lib/library-publication-index.test.ts +++ b/src/lib/library-publication-index.test.ts @@ -253,6 +253,34 @@ describe('library-publication-index', () => { expect(picked.every((e) => e.engagementCount === 0)).toBe(true) }) + it('pickLibraryPublicationEntries merges engaged roots with recent feed', () => { + const engagedRoot = indexEvent('engaged', [`30041:${PK}:a`], '1'.repeat(64)) + engagedRoot.created_at = 5 + const recentRoots = Array.from({ length: 5 }, (_, i) => { + const ev = indexEvent(`recent-${i}`, [`30041:${PK}:r-${i}`], String(i + 2).padEnd(64, '0').slice(0, 64)) + ev.created_at = 100 + i + return ev + }) + const roots = [engagedRoot, ...recentRoots] + const indexByAddress = buildIndexByAddress(roots) + const label: Event = { + id: '4'.repeat(64), + kind: ExtendedKind.LABEL, + pubkey: 'f'.repeat(64), + created_at: 50, + content: '', + tags: [['L', 'ugc'], ['l', 'booklist', 'ugc'], ['e', engagedRoot.id]], + sig: 'e'.repeat(128) + } + const engagement = buildEngagementMapsFromEvents([label], [], []) + + const picked = pickLibraryPublicationEntries(roots, indexByAddress, engagement) + + expect(picked.length).toBeGreaterThan(1) + expect(picked.some((e) => e.event.id === engagedRoot.id && e.hasBooklistLabel)).toBe(true) + expect(picked.some((e) => e.event.id === recentRoots[4].id)).toBe(true) + }) + it('buildRecentPublicationEntries caps at limit', () => { const roots = Array.from({ length: 12 }, (_, i) => { const ev = indexEvent(`book-${i}`, [`30041:${PK}:ch-${i}`], String(i).padEnd(64, '0').slice(0, 64)) diff --git a/src/lib/library-publication-index.ts b/src/lib/library-publication-index.ts index ff5ba5f5..f30dfa57 100644 --- a/src/lib/library-publication-index.ts +++ b/src/lib/library-publication-index.ts @@ -700,18 +700,30 @@ export function buildRecentPublicationEntries( .map((event) => buildLibraryPublicationEntry(event, indexByAddress, engagement)) } -/** Engaged publications first; when none match, show the newest top-level indexes (still enriched). */ +/** Engaged publications first, then fill with newest top-level indexes up to {@link LIBRARY_RECENT_FALLBACK_LIMIT}. */ export function pickLibraryPublicationEntries( roots: Event[], indexByAddress: Map, engagement: PublicationEngagementMaps ): LibraryPublicationEntry[] { - const enriched = getTopLevelIndexEvents(roots).map((root) => - buildLibraryPublicationEntry(root, indexByAddress, engagement) - ) - const engaged = enriched.filter((entry) => entry.hasLabel || entry.hasComment || entry.hasHighlight) - if (engaged.length > 0) return sortLibraryPublications(engaged) - return sortLibraryPublications(buildRecentPublicationEntries(roots, indexByAddress, engagement)) + const engaged = filterEngagedPublications(roots, indexByAddress, engagement) + const recent = buildRecentPublicationEntries(roots, indexByAddress, engagement) + if (engaged.length === 0) return sortLibraryPublications(recent) + + const seen = new Set() + const merged: LibraryPublicationEntry[] = [] + for (const entry of sortLibraryPublications(engaged)) { + if (seen.has(entry.event.id)) continue + seen.add(entry.event.id) + merged.push(entry) + } + for (const entry of recent) { + if (merged.length >= LIBRARY_RECENT_FALLBACK_LIMIT) break + if (seen.has(entry.event.id)) continue + seen.add(entry.event.id) + merged.push(entry) + } + return merged } export function sortLibraryPublications(entries: LibraryPublicationEntry[]): LibraryPublicationEntry[] {