diff --git a/src/components/Image/index.tsx b/src/components/Image/index.tsx
index 035008ba..1031d8d6 100644
--- a/src/components/Image/index.tsx
+++ b/src/components/Image/index.tsx
@@ -229,19 +229,6 @@ export default function Image({
if (el.complete && el.naturalWidth > 0) {
captureIntrinsicDim(el)
notifyLoaded()
- return
- }
- if (typeof el.decode === 'function') {
- let cancelled = false
- el.decode().then(() => {
- if (!cancelled && el.naturalWidth > 0) {
- captureIntrinsicDim(el)
- notifyLoaded()
- }
- }).catch(() => {})
- return () => {
- cancelled = true
- }
}
}, [revealed, badSrc, imageUrl, notifyLoaded, captureIntrinsicDim])
@@ -398,7 +385,7 @@ export default function Image({
src={imageUrl}
alt={finalAlt}
referrerPolicy="no-referrer-when-downgrade"
- decoding={effectiveHoldUntilClick ? 'async' : 'sync'}
+ decoding="async"
// `lazy` often never starts the request inside nested feed scrollers; always-load should fetch eagerly.
loading="eager"
{...(fetchPriority ? { fetchpriority: fetchPriority } : {})}
diff --git a/src/components/Note/PublicationCoverImage.tsx b/src/components/Note/PublicationCoverImage.tsx
index b3e23dd0..a06f127f 100644
--- a/src/components/Note/PublicationCoverImage.tsx
+++ b/src/components/Note/PublicationCoverImage.tsx
@@ -38,7 +38,7 @@ export default function PublicationCoverImage({
diff --git a/src/hooks/useLibraryPublications.ts b/src/hooks/useLibraryPublications.ts
index 010df0a4..a14b3571 100644
--- a/src/hooks/useLibraryPublications.ts
+++ b/src/hooks/useLibraryPublications.ts
@@ -11,10 +11,12 @@ import {
type LibraryPublicationEntry,
type PublicationEngagementMaps
} from '@/lib/library-publication-index'
-import { BOOKLIST_LABEL_UPDATED_EVENT } from '@/lib/booklist-label'
+import { BOOKLIST_LABEL_UPDATED_EVENT, fetchViewerBooklistTargets } from '@/lib/booklist-label'
+import { buildAccountListRelayUrlsForMerge } from '@/lib/account-list-relay-urls'
import { fetchNewestPinListForPubkey } from '@/lib/replaceable-list-latest'
import { getTopLevelIndexEvents } from '@/lib/publication-index'
import logger from '@/lib/logger'
+import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider'
import { useNostr } from '@/providers/NostrProvider'
import type { Event } from 'nostr-tools'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -39,8 +41,11 @@ const EMPTY_ENGAGEMENT: PublicationEngagementMaps = {
highlightAddresses: new Set()
}
+const EMPTY_BOOKLIST_TARGETS = { addresses: new Set(), eventIds: new Set() }
+
export function useLibraryPublications(isActive: boolean) {
const { pubkey, bookmarkListEvent } = useNostr()
+ const { favoriteRelays, blockedRelays } = useFavoriteRelays()
const [entries, setEntries] = useState([])
const [indexEvents, setIndexEvents] = useState([])
const [engagement, setEngagement] = useState(EMPTY_ENGAGEMENT)
@@ -56,8 +61,31 @@ export function useLibraryPublications(isActive: boolean) {
const [allIndexCount, setAllIndexCount] = useState(0)
const [topLevelCount, setTopLevelCount] = useState(0)
const [pinListEvent, setPinListEvent] = useState(null)
+ const [myBooklistTargets, setMyBooklistTargets] = useState(EMPTY_BOOKLIST_TARGETS)
const loadGenRef = useRef(0)
+ const loadMyBooklistTargets = useCallback(async () => {
+ if (!pubkey) {
+ setMyBooklistTargets(EMPTY_BOOKLIST_TARGETS)
+ return
+ }
+ const relays = await buildAccountListRelayUrlsForMerge({
+ accountPubkey: pubkey,
+ favoriteRelays: favoriteRelays ?? [],
+ blockedRelays: blockedRelays ?? []
+ })
+ const targets = await fetchViewerBooklistTargets(pubkey, relays)
+ setMyBooklistTargets(targets)
+ }, [pubkey, favoriteRelays, blockedRelays])
+
+ useEffect(() => {
+ if (!isActive || !pubkey) {
+ setMyBooklistTargets(EMPTY_BOOKLIST_TARGETS)
+ return
+ }
+ void loadMyBooklistTargets()
+ }, [isActive, pubkey, loadMyBooklistTargets])
+
useEffect(() => {
if (!pubkey) {
setPinListEvent(null)
@@ -65,9 +93,9 @@ export function useLibraryPublications(isActive: boolean) {
}
let cancelled = false
void (async () => {
- const relays = await buildLibraryRelayUrls(pubkey)
+ const relays = await buildLibraryRelayUrls(pubkey, blockedRelays ?? [])
const pinList = await fetchNewestPinListForPubkey(pubkey, relays)
- if (!cancelled) setPinListEvent(pinList)
+ if (!cancelled) setPinListEvent(pinList ?? null)
})()
return () => {
cancelled = true
@@ -89,7 +117,7 @@ export function useLibraryPublications(isActive: boolean) {
logger.info('[Library] page load requested', { forceRefresh, gen })
}
try {
- const relays = await buildLibraryRelayUrls(pubkey || undefined)
+ const relays = await buildLibraryRelayUrls(pubkey || undefined, blockedRelays ?? [])
let timeoutId: number | undefined
const timeoutPromise = new Promise((_, reject) => {
timeoutId = window.setTimeout(() => reject(new Error('Library load timed out')), LOAD_TIMEOUT_MS)
@@ -134,7 +162,7 @@ export function useLibraryPublications(isActive: boolean) {
}
}
},
- [pubkey]
+ [pubkey, blockedRelays]
)
useEffect(() => {
@@ -147,7 +175,8 @@ export function useLibraryPublications(isActive: boolean) {
let cancelled = false
const onBooklistUpdated = () => {
void (async () => {
- const relays = await buildLibraryRelayUrls(pubkey)
+ await loadMyBooklistTargets()
+ const relays = await buildLibraryRelayUrls(pubkey, blockedRelays ?? [])
const { engagement: nextEngagement, engaged } = await refreshLibraryEngagement(
relays,
indexEvents,
@@ -165,7 +194,7 @@ export function useLibraryPublications(isActive: boolean) {
cancelled = true
window.removeEventListener(BOOKLIST_LABEL_UPDATED_EVENT, onBooklistUpdated)
}
- }, [isActive, pubkey, indexEvents, debouncedSearch])
+ }, [isActive, pubkey, indexEvents, debouncedSearch, loadMyBooklistTargets, blockedRelays])
useEffect(() => {
const q = debouncedSearch.trim()
@@ -205,8 +234,8 @@ export function useLibraryPublications(isActive: boolean) {
setRelaySearchLoading(true)
setError(null)
try {
- const relays = await buildLibraryRelayUrls(pubkey || undefined)
- const { events, mergedIndexEvents, entries, fromCache } = await searchLibraryPublicationsOnRelays(
+ const relays = await buildLibraryRelayUrls(pubkey || undefined, blockedRelays ?? [])
+ const { events, mergedIndexEvents, fromCache } = await searchLibraryPublicationsOnRelays(
q,
relays,
{ indexEvents, engagement }
@@ -220,6 +249,18 @@ export function useLibraryPublications(isActive: boolean) {
fromCache
})
}
+
+ let nextEngagement = engagement
+ if (pubkey) {
+ const refreshed = await refreshLibraryEngagement(relays, mergedIndexEvents, pubkey)
+ nextEngagement = refreshed.engagement
+ setEngagement(nextEngagement)
+ }
+
+ const entries = await searchLibraryPublications(q, {
+ indexEvents: mergedIndexEvents,
+ engagement: nextEngagement
+ })
setSearchResults(entries)
} catch (e) {
const message = e instanceof Error ? e.message : 'Relay search failed'
@@ -230,11 +271,16 @@ export function useLibraryPublications(isActive: boolean) {
} finally {
setRelaySearchLoading(false)
}
- }, [searchQuery, pubkey, indexEvents, engagement])
+ }, [searchQuery, pubkey, indexEvents, engagement, blockedRelays])
const filteredEntries = useMemo(() => {
const q = debouncedSearch.trim()
- const mineFilterOpts = { bookmarkListEvent, pinListEvent }
+ const mineFilterOpts = {
+ bookmarkListEvent,
+ pinListEvent,
+ myBooklistAddresses: myBooklistTargets.addresses,
+ myBooklistEventIds: myBooklistTargets.eventIds
+ }
let list: LibraryPublicationEntry[]
if (showOnlyMine && !q) {
list = filterLibraryPublicationsByUser(
@@ -258,7 +304,8 @@ export function useLibraryPublications(isActive: boolean) {
indexEvents,
engagement,
bookmarkListEvent,
- pinListEvent
+ pinListEvent,
+ myBooklistTargets
])
return {
diff --git a/src/lib/booklist-label.ts b/src/lib/booklist-label.ts
index fa842394..c6a46fef 100644
--- a/src/lib/booklist-label.ts
+++ b/src/lib/booklist-label.ts
@@ -9,6 +9,57 @@ import type { Event, Filter } from 'nostr-tools'
export const BOOKLIST_LABEL_UPDATED_EVENT = 'booklist-label-updated'
+export type ViewerBooklistTargets = {
+ addresses: Set
+ eventIds: Set
+}
+
+function collectBooklistTargetsFromLabelEvents(events: Event[]): ViewerBooklistTargets {
+ const addresses = new Set()
+ const eventIds = new Set()
+ for (const ev of events) {
+ if (!labelEventHasBooklistTag(ev)) continue
+ for (const tag of ev.tags) {
+ if (tag[0] === 'a' && tag[1]?.trim()) addresses.add(tag[1].trim())
+ if (tag[0] === 'e' && tag[1]?.trim()) eventIds.add(tag[1].trim().toLowerCase())
+ }
+ }
+ return { addresses, eventIds }
+}
+
+/** All publication coordinates the viewer has on their booklist (session + network). */
+export async function fetchViewerBooklistTargets(
+ userPubkey: string,
+ relayUrls: string[]
+): Promise {
+ const sessionHits = eventService.listSessionEventsAuthoredBy(userPubkey, {
+ kinds: [ExtendedKind.LABEL],
+ limit: 200
+ })
+ const merged = new Map()
+ for (const ev of sessionHits) {
+ if (labelEventHasBooklistTag(ev)) merged.set(ev.id, ev)
+ }
+
+ if (relayUrls.length > 0) {
+ const filter: Filter = {
+ kinds: [ExtendedKind.LABEL],
+ authors: [userPubkey],
+ '#l': [NIP32_BOOKLIST_LABEL],
+ limit: 500
+ }
+ const network = await client.fetchEvents(relayUrls, [filter], {
+ globalTimeout: 12_000,
+ eoseTimeout: 3_000
+ })
+ for (const ev of network) {
+ if (labelEventHasBooklistTag(ev)) merged.set(ev.id, ev)
+ }
+ }
+
+ return collectBooklistTargetsFromLabelEvents([...merged.values()])
+}
+
export function dispatchBooklistLabelUpdated(publication: Event): void {
window.dispatchEvent(
new CustomEvent(BOOKLIST_LABEL_UPDATED_EVENT, {
diff --git a/src/lib/event-metadata.publication-index.test.ts b/src/lib/event-metadata.publication-index.test.ts
index 0cf0de4f..e93a4c9b 100644
--- a/src/lib/event-metadata.publication-index.test.ts
+++ b/src/lib/event-metadata.publication-index.test.ts
@@ -85,4 +85,15 @@ describe('getPublicationIndexMetadataFromEvent', () => {
const meta = getPublicationIndexMetadataFromEvent(event)
expect(meta.image).toBe('https://example.com/cover.jpg')
})
+
+ it('normalizes Gutenberg ebook page in image tag to cover JPG', () => {
+ const event = indexEvent([
+ ['d', 'book'],
+ ['title', 'Book'],
+ ['image', 'https://www.gutenberg.org/ebooks/16702'],
+ ['a', `30041:${PK}:intro`]
+ ])
+ const meta = getPublicationIndexMetadataFromEvent(event)
+ expect(meta.image).toBe('https://www.gutenberg.org/cache/epub/16702/pg16702.cover.medium.jpg')
+ })
})
diff --git a/src/lib/event-metadata.ts b/src/lib/event-metadata.ts
index 8cb67790..37b480dd 100644
--- a/src/lib/event-metadata.ts
+++ b/src/lib/event-metadata.ts
@@ -2,7 +2,7 @@ import { ExtendedKind, FAST_READ_RELAY_URLS, FAST_WRITE_RELAY_URLS, POLL_TYPE }
import { TEmoji, TMailboxRelay, TPollType, TRelayList, TRelaySet, TPaymentInfo, TProfile } from '@/types'
import { Event, kinds } from 'nostr-tools'
import { buildATag } from './draft-event'
-import { resolveGutenbergCoverImageUrl } from './gutenberg-cover'
+import { normalizeGutenbergCoverImageUrl, resolveGutenbergCoverImageUrl } from './gutenberg-cover'
import { getLatestEvent, getReplaceableEventIdentifier } from './event'
import { getAmountFromInvoice, getLightningAddressFromProfile } from './lightning'
import { formatPubkey, pubkeyToNpub } from './pubkey'
@@ -720,7 +720,9 @@ export function getPublicationIndexMetadataFromEvent(event: Event): PublicationI
}
let image = base.image?.trim() || undefined
- if (!image) {
+ if (image) {
+ image = normalizeGutenbergCoverImageUrl(image)
+ } else {
image = resolveGutenbergCoverImageUrl(source)
}
diff --git a/src/lib/gutenberg-cover.test.ts b/src/lib/gutenberg-cover.test.ts
index cb9ae631..3773a332 100644
--- a/src/lib/gutenberg-cover.test.ts
+++ b/src/lib/gutenberg-cover.test.ts
@@ -1,6 +1,7 @@
import { describe, expect, it } from 'vitest'
import {
gutenbergCoverImageUrl,
+ normalizeGutenbergCoverImageUrl,
parseGutenbergEbookId,
resolveGutenbergCoverImageUrl
} from '@/lib/gutenberg-cover'
@@ -12,6 +13,9 @@ describe('gutenberg-cover', () => {
expect(parseGutenbergEbookId('https://www.gutenberg.org/files/21020/21020-h/21020-h.htm')).toBe(
'21020'
)
+ expect(
+ parseGutenbergEbookId('https://www.gutenberg.org/cache/epub/16702/pg16702.cover.medium.jpg')
+ ).toBe('16702')
})
it('builds medium cover URL', () => {
@@ -26,4 +30,18 @@ describe('gutenberg-cover', () => {
).toBe('https://www.gutenberg.org/cache/epub/58363/pg58363.cover.medium.jpg')
expect(resolveGutenbergCoverImageUrl('https://example.com/book')).toBeUndefined()
})
+
+ it('normalizeGutenbergCoverImageUrl converts ebook pages to cover JPG', () => {
+ expect(normalizeGutenbergCoverImageUrl('https://www.gutenberg.org/ebooks/16702')).toBe(
+ 'https://www.gutenberg.org/cache/epub/16702/pg16702.cover.medium.jpg'
+ )
+ expect(
+ normalizeGutenbergCoverImageUrl(
+ 'https://www.gutenberg.org/cache/epub/16702/pg16702.cover.medium.jpg'
+ )
+ ).toBe('https://www.gutenberg.org/cache/epub/16702/pg16702.cover.medium.jpg')
+ expect(normalizeGutenbergCoverImageUrl('https://example.com/cover.jpg')).toBe(
+ 'https://example.com/cover.jpg'
+ )
+ })
})
diff --git a/src/lib/gutenberg-cover.ts b/src/lib/gutenberg-cover.ts
index 48dd3b8f..43077cd3 100644
--- a/src/lib/gutenberg-cover.ts
+++ b/src/lib/gutenberg-cover.ts
@@ -2,11 +2,14 @@
const GUTENBERG_EBOOK_URL = /gutenberg\.org\/ebooks\/(\d+)/i
const GUTENBERG_FILES_URL = /gutenberg\.org\/files\/(\d+)/i
+const GUTENBERG_CACHE_URL = /gutenberg\.org\/cache\/epub\/(\d+)/i
+
+const DIRECT_IMAGE_EXT = /\.(?:jpe?g|png|gif|webp|avif)(?:[?#]|$)/i
export function parseGutenbergEbookId(source: string): string | null {
const trimmed = source.trim()
if (!trimmed) return null
- for (const pattern of [GUTENBERG_EBOOK_URL, GUTENBERG_FILES_URL]) {
+ for (const pattern of [GUTENBERG_EBOOK_URL, GUTENBERG_FILES_URL, GUTENBERG_CACHE_URL]) {
const match = trimmed.match(pattern)
if (match?.[1]) return match[1]
}
@@ -26,3 +29,16 @@ export function resolveGutenbergCoverImageUrl(source: string | undefined): strin
if (!id) return undefined
return gutenbergCoverImageUrl(id)
}
+
+/**
+ * Normalize a publication `image` tag URL. Gutenberg ebook/files pages become cache cover JPGs;
+ * direct `.jpg` / cache cover URLs are kept as-is.
+ */
+export function normalizeGutenbergCoverImageUrl(url: string): string {
+ const trimmed = url.trim()
+ if (!trimmed.toLowerCase().includes('gutenberg')) return trimmed
+ const id = parseGutenbergEbookId(trimmed)
+ if (!id) return trimmed
+ if (DIRECT_IMAGE_EXT.test(trimmed)) return trimmed
+ return gutenbergCoverImageUrl(id)
+}
diff --git a/src/lib/library-publication-index.test.ts b/src/lib/library-publication-index.test.ts
index e2f65de6..ceef187b 100644
--- a/src/lib/library-publication-index.test.ts
+++ b/src/lib/library-publication-index.test.ts
@@ -338,4 +338,48 @@ describe('library-publication-index', () => {
[authored.id, booklisted.id, commented.id, unrelated.id].sort()
)
})
+
+ it('searchLibraryPublications keeps my booklist flags for booklist-only publications', async () => {
+ clearLibrarySearchSessionCache()
+ const viewerPk = 'f'.repeat(64)
+ const rootAddr = `30040:${PK}:jane-eyre`
+ const root = indexEvent('jane-eyre', [`30041:${PK}:intro`], '9'.repeat(64))
+ root.tags = [['d', 'jane-eyre'], ['title', 'Jane Eyre'], ['a', `30041:${PK}:intro`]]
+ const label: Event = {
+ id: '7'.repeat(64),
+ kind: ExtendedKind.LABEL,
+ pubkey: viewerPk,
+ created_at: 50,
+ content: '',
+ tags: [['L', 'ugc'], ['l', 'booklist', 'ugc'], ['a', rootAddr]],
+ sig: 'e'.repeat(128)
+ }
+ const engagement = buildEngagementMapsFromEvents([label], [], [], undefined, undefined, viewerPk)
+ const results = await searchLibraryPublications('jane eyre', { indexEvents: [root], engagement })
+ expect(results).toHaveLength(1)
+ expect(results[0].hasMyBooklistLabel).toBe(true)
+ expect(filterLibraryPublicationsByUser(results, viewerPk)).toHaveLength(1)
+ })
+
+ it('filterLibraryPublicationsByUser matches myBooklistAddresses without engagement flags', () => {
+ const viewerPk = 'f'.repeat(64)
+ const rootAddr = `30040:${PK}:jane-eyre`
+ const root = indexEvent('jane-eyre', [`30041:${PK}:intro`], '9'.repeat(64))
+ const entry = {
+ event: root,
+ hasLabel: false,
+ labelNames: [],
+ hasBooklistLabel: false,
+ hasMyBooklistLabel: false,
+ hasMyComment: false,
+ hasMyHighlight: false,
+ hasComment: false,
+ hasHighlight: false,
+ engagementCount: 0
+ }
+ const filtered = filterLibraryPublicationsByUser([entry], viewerPk, {
+ myBooklistAddresses: new Set([rootAddr])
+ })
+ expect(filtered).toHaveLength(1)
+ })
})
diff --git a/src/lib/library-publication-index.ts b/src/lib/library-publication-index.ts
index aa1484bf..0eaf5f10 100644
--- a/src/lib/library-publication-index.ts
+++ b/src/lib/library-publication-index.ts
@@ -1,3 +1,4 @@
+import { findSessionBooklistLabelForPublication } from '@/lib/booklist-label'
import { ExtendedKind, LIBRARY_RELAY_URLS } from '@/constants'
import {
eventMatchesGeneralSearchQuery,
@@ -21,6 +22,7 @@ import {
} from '@/lib/publication-index'
import { getReplaceableCoordinateFromEvent, isReplaceableEvent } from '@/lib/event'
import { isEventInPinList } from '@/lib/replaceable-list-latest'
+import { isRelayBlockedByUser } from '@/lib/relay-blocked'
import { buildComprehensiveRelayList } from '@/lib/relay-list-builder'
import {
clearLibraryIndexIdbCache,
@@ -117,7 +119,15 @@ function librarySearchFingerprint(context: LibrarySearchContext): string {
? engagement.labelAddresses.size +
engagement.labelEventIds.size +
engagement.commentAddresses.size +
- engagement.highlightAddresses.size
+ engagement.highlightAddresses.size +
+ engagement.booklistAddresses.size +
+ engagement.booklistEventIds.size +
+ engagement.myBooklistAddresses.size +
+ engagement.myBooklistEventIds.size +
+ engagement.myCommentAddresses.size +
+ engagement.myCommentEventIds.size +
+ engagement.myHighlightAddresses.size +
+ engagement.myHighlightEventIds.size
: 0
return `${context.indexEvents.length}:${engagementSize}`
}
@@ -247,23 +257,38 @@ function normalizeLibraryRelayUrl(url: string): string {
return normalizeUrl(trimmed) || trimmed
}
-function libraryIndexRelayUrls(extraRelayUrls: string[] = []): string[] {
- const base = LIBRARY_RELAY_URLS.map(normalizeLibraryRelayUrl).filter(Boolean)
- const extra = extraRelayUrls.map(normalizeLibraryRelayUrl).filter(Boolean)
+function filterBlockedLibraryRelays(urls: string[], blockedRelays: readonly string[] = []): string[] {
+ if (blockedRelays.length === 0) return urls
+ return urls.filter((url) => !isRelayBlockedByUser(url, blockedRelays))
+}
+
+function libraryIndexRelayUrls(extraRelayUrls: string[] = [], blockedRelays: readonly string[] = []): string[] {
+ const base = filterBlockedLibraryRelays(
+ LIBRARY_RELAY_URLS.map(normalizeLibraryRelayUrl).filter(Boolean),
+ blockedRelays
+ )
+ const extra = filterBlockedLibraryRelays(
+ extraRelayUrls.map(normalizeLibraryRelayUrl).filter(Boolean),
+ blockedRelays
+ )
return [...new Set([...base, ...extra])]
}
-export async function buildLibraryRelayUrls(userPubkey?: string): Promise {
- const base = libraryIndexRelayUrls()
+export async function buildLibraryRelayUrls(
+ userPubkey?: string,
+ blockedRelays: string[] = []
+): Promise {
+ const base = libraryIndexRelayUrls([], blockedRelays)
const urls = await buildComprehensiveRelayList({
userPubkey,
includeUserOwnRelays: true,
includeFastReadRelays: false,
includeSearchableRelays: false,
includeFavoriteRelays: false,
- relayHints: base
+ relayHints: base,
+ blockedRelays
})
- return libraryIndexRelayUrls([...urls])
+ return libraryIndexRelayUrls([...urls], blockedRelays)
}
export async function fetchLibraryIndexEvents(relayUrls: string[]): Promise {
@@ -733,13 +758,19 @@ export function publicationEntryBelongsToUser(
userPubkey: string
bookmarkListEvent?: Event | null
pinListEvent?: Event | null
+ myBooklistAddresses?: Set
+ myBooklistEventIds?: Set
}
): boolean {
const { event } = entry
const pk = opts.userPubkey.toLowerCase()
+ const rootAddr = eventTagAddress(event)
if (event.pubkey.toLowerCase() === pk) return true
if (event.tags.some((t) => t[0] === 'p' && t[1]?.toLowerCase() === pk)) return true
if (entry.hasMyBooklistLabel || entry.hasMyComment || entry.hasMyHighlight) return true
+ if (rootAddr && opts.myBooklistAddresses?.has(rootAddr)) return true
+ if (opts.myBooklistEventIds?.has(event.id.toLowerCase())) return true
+ if (findSessionBooklistLabelForPublication(opts.userPubkey, event)) return true
if (opts.bookmarkListEvent && isEventInBookmarkList(opts.bookmarkListEvent, event)) return true
if (opts.pinListEvent && isEventInPinList(opts.pinListEvent, event)) return true
return false
@@ -806,20 +837,7 @@ function libraryEntriesFromRoots(
indexByAddress: Map,
engagement: PublicationEngagementMaps
): LibraryPublicationEntry[] {
- return roots.map((root) => {
- const engaged = filterEngagedPublications([root], indexByAddress, engagement)
- if (engaged.length > 0) return engaged[0]
- return {
- event: root,
- hasLabel: false,
- labelNames: [],
- hasBooklistLabel: false,
- hasMyBooklistLabel: false,
- hasComment: false,
- hasHighlight: false,
- engagementCount: 0
- }
- })
+ return roots.map((root) => buildLibraryPublicationEntry(root, indexByAddress, engagement))
}
/** Re-fetch engagement maps for the current library index snapshot (e.g. after booklist toggle). */
@@ -1213,6 +1231,8 @@ export function filterLibraryPublicationsByUser(
opts?: {
bookmarkListEvent?: Event | null
pinListEvent?: Event | null
+ myBooklistAddresses?: Set
+ myBooklistEventIds?: Set
}
): LibraryPublicationEntry[] {
if (!userPubkey) return entries
@@ -1220,7 +1240,9 @@ export function filterLibraryPublicationsByUser(
publicationEntryBelongsToUser(entry, {
userPubkey,
bookmarkListEvent: opts?.bookmarkListEvent,
- pinListEvent: opts?.pinListEvent
+ pinListEvent: opts?.pinListEvent,
+ myBooklistAddresses: opts?.myBooklistAddresses,
+ myBooklistEventIds: opts?.myBooklistEventIds
})
)
}
diff --git a/vite.config.ts b/vite.config.ts
index 67dd7809..c7333369 100644
--- a/vite.config.ts
+++ b/vite.config.ts
@@ -527,6 +527,12 @@ export default defineConfig(({ mode }) => {
cacheableResponse: { statuses: [200] }
}
},
+ {
+ // Project Gutenberg covers: bypass SW cache — CacheFirst can serve stale/truncated
+ // HTML error bodies for .jpg URLs and the browser reports "image corrupt or truncated".
+ urlPattern: /^https:\/\/(?:www\.)?gutenberg\.org\//i,
+ handler: 'NetworkOnly'
+ },
{
// Generic cross-origin images by file extension (covers hosts not matched above)
urlPattern: /^https?:\/\/.+\.(?:png|jpg|jpeg|gif|webp|avif|svg|ico)(?:\?.*)?$/i,