Browse Source

bug-fixes

imwald
Silberengel 1 week ago
parent
commit
b49acd2057
  1. 9
      src/components/Library/LibraryPublicationGrid.tsx
  2. 17
      src/hooks/usePanelMode.test.ts
  3. 26
      src/hooks/usePanelMode.ts
  4. 28
      src/lib/library-publication-index.test.ts
  5. 26
      src/lib/library-publication-index.ts

9
src/components/Library/LibraryPublicationGrid.tsx

@ -1,8 +1,10 @@
import PublicationCard from '@/components/Note/PublicationCard' import PublicationCard from '@/components/Note/PublicationCard'
import { Skeleton } from '@/components/ui/skeleton' import { Skeleton } from '@/components/ui/skeleton'
import { libraryPublicationGridColumnClass, usePanelMode } from '@/hooks/usePanelMode'
import type { LibraryPublicationEntry } from '@/lib/library-publication-index' import type { LibraryPublicationEntry } from '@/lib/library-publication-index'
import { isBooklistNip32Label } from '@/lib/nip32-label' import { isBooklistNip32Label } from '@/lib/nip32-label'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import { BookOpen, Highlighter, MessageSquare, Tag } from 'lucide-react' import { BookOpen, Highlighter, MessageSquare, Tag } from 'lucide-react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
@ -95,10 +97,13 @@ export default function LibraryPublicationGrid({
emptyMessage?: string emptyMessage?: string
}) { }) {
const { t } = useTranslation() const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const panelMode = usePanelMode()
const gridCols = libraryPublicationGridColumnClass(isSmallScreen, panelMode)
if (loading) { if (loading) {
return ( return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3"> <div className={cn('grid gap-4', gridCols)}>
{Array.from({ length: 6 }).map((_, i) => ( {Array.from({ length: 6 }).map((_, i) => (
<Skeleton key={i} className="h-48 w-full rounded-lg" /> <Skeleton key={i} className="h-48 w-full rounded-lg" />
))} ))}
@ -115,7 +120,7 @@ export default function LibraryPublicationGrid({
} }
return ( return (
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 xl:grid-cols-3"> <div className={cn('grid gap-4', gridCols)}>
{entries.map((entry) => ( {entries.map((entry) => (
<div <div
key={entry.event.id} key={entry.event.id}

17
src/hooks/usePanelMode.test.ts

@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest'
import { libraryPublicationGridColumnClass } from '@/hooks/usePanelMode'
describe('libraryPublicationGridColumnClass', () => {
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')
})
})

26
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'
}

28
src/lib/library-publication-index.test.ts

@ -253,6 +253,34 @@ describe('library-publication-index', () => {
expect(picked.every((e) => e.engagementCount === 0)).toBe(true) 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', () => { it('buildRecentPublicationEntries caps at limit', () => {
const roots = Array.from({ length: 12 }, (_, i) => { const roots = Array.from({ length: 12 }, (_, i) => {
const ev = indexEvent(`book-${i}`, [`30041:${PK}:ch-${i}`], String(i).padEnd(64, '0').slice(0, 64)) const ev = indexEvent(`book-${i}`, [`30041:${PK}:ch-${i}`], String(i).padEnd(64, '0').slice(0, 64))

26
src/lib/library-publication-index.ts

@ -700,18 +700,30 @@ export function buildRecentPublicationEntries(
.map((event) => buildLibraryPublicationEntry(event, indexByAddress, engagement)) .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( export function pickLibraryPublicationEntries(
roots: Event[], roots: Event[],
indexByAddress: Map<string, Event>, indexByAddress: Map<string, Event>,
engagement: PublicationEngagementMaps engagement: PublicationEngagementMaps
): LibraryPublicationEntry[] { ): LibraryPublicationEntry[] {
const enriched = getTopLevelIndexEvents(roots).map((root) => const engaged = filterEngagedPublications(roots, indexByAddress, engagement)
buildLibraryPublicationEntry(root, indexByAddress, engagement) const recent = buildRecentPublicationEntries(roots, indexByAddress, engagement)
) if (engaged.length === 0) return sortLibraryPublications(recent)
const engaged = enriched.filter((entry) => entry.hasLabel || entry.hasComment || entry.hasHighlight)
if (engaged.length > 0) return sortLibraryPublications(engaged) const seen = new Set<string>()
return sortLibraryPublications(buildRecentPublicationEntries(roots, indexByAddress, engagement)) 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[] { export function sortLibraryPublications(entries: LibraryPublicationEntry[]): LibraryPublicationEntry[] {

Loading…
Cancel
Save