diff --git a/src/hooks/useLibraryPublications.ts b/src/hooks/useLibraryPublications.ts index 37258d3e..e5999d94 100644 --- a/src/hooks/useLibraryPublications.ts +++ b/src/hooks/useLibraryPublications.ts @@ -23,6 +23,7 @@ import { useFavoriteRelays } from '@/providers/FavoriteRelaysProvider' import { useNostr } from '@/providers/NostrProvider' import type { Event } from 'nostr-tools' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useTranslation } from 'react-i18next' const SEARCH_DEBOUNCE_MS = 300 const RELAY_SEARCH_TIMEOUT_MS = 30_000 @@ -53,6 +54,7 @@ const EMPTY_ENGAGEMENT: PublicationEngagementMaps = { const EMPTY_BOOKLIST_TARGETS = { addresses: new Set(), eventIds: new Set() } export function useLibraryPublications(isActive: boolean) { + const { t } = useTranslation() const { pubkey, bookmarkListEvent } = useNostr() const { favoriteRelays, blockedRelays } = useFavoriteRelays() const [entries, setEntries] = useState([]) @@ -374,14 +376,22 @@ export function useLibraryPublications(isActive: boolean) { setSearchResults(entries) } catch (e) { const message = e instanceof Error ? e.message : 'Relay search failed' - setError(message) + const local = await searchLibraryPublications(q, { indexEvents, engagement }, searchAxis) + if (local.length > 0) { + setSearchResults(local) + setError(null) + } else { + setError( + message === 'Relay search timed out' ? t('Library relay search timed out') : message + ) + } if (import.meta.env.DEV) { - logger.warn('[Library] relay search failed', { message }) + logger.warn('[Library] relay search failed', { message, localFallback: local.length }) } } finally { setRelaySearchLoading(false) } - }, [searchQuery, searchAxis, pubkey, indexEvents, engagement, blockedRelays]) + }, [searchQuery, searchAxis, pubkey, indexEvents, engagement, blockedRelays, t]) const mineFilterOpts = useMemo( () => ({ diff --git a/src/i18n/locales/de.ts b/src/i18n/locales/de.ts index 25629052..a2256c6b 100644 --- a/src/i18n/locales/de.ts +++ b/src/i18n/locales/de.ts @@ -1658,6 +1658,7 @@ export default { 'Library search scope author': 'Suche nach Autor', 'Library search scope dtag': 'Suche nach d-Tag', 'Library search commit hint': 'Enter drücken oder einen Suchtyp auswählen', + 'Library relay search timed out': 'Relay-Suche abgelaufen. Treffer aus dem lokalen Bibliotheks-Cache werden angezeigt, sofern vorhanden.', 'Library show only my publications': 'Meine Publikationen', 'Library empty': 'Noch keine Publikationen auf deinen Relays gefunden.', 'Library empty filtered': 'Keine Publikationen entsprechen den Filtern.', diff --git a/src/i18n/locales/en.ts b/src/i18n/locales/en.ts index 63b0f291..9b256a1d 100644 --- a/src/i18n/locales/en.ts +++ b/src/i18n/locales/en.ts @@ -1681,6 +1681,7 @@ export default { 'Library search scope author': 'Searching by author', 'Library search scope dtag': 'Searching by d-tag', 'Library search commit hint': 'Press Enter or choose a search type below', + 'Library relay search timed out': 'Relay search timed out. Showing matches from your local library cache when available.', 'Library show only my publications': 'My publications', 'Library empty': 'No publications found on your relays yet.', 'Library empty filtered': 'No publications match your filters.', diff --git a/src/lib/library-publication-index.test.ts b/src/lib/library-publication-index.test.ts index 296114d9..f13de94c 100644 --- a/src/lib/library-publication-index.test.ts +++ b/src/lib/library-publication-index.test.ts @@ -282,7 +282,24 @@ describe('library-publication-index', () => { expect(byAuthor).toHaveLength(1) expect(filterEventsForPublicationRelaySearchAxis([root], 'title', 'charlotte')).toHaveLength(0) + expect(filterEventsForPublicationRelaySearchAxis([root], 'author', 'charlotte')).toHaveLength(1) expect(publicationMetadataTagMatchesQuery(root, 'title', 'Jane Eyre')).toBe(true) + expect(publicationMetadataTagMatchesQuery(root, 'author', 'Brontë')).toBe(true) + }) + + it('author and title axes match partial metadata text but d-tag stays exact', () => { + const root = indexEvent('faust', [`30041:${PK}:intro`]) + root.tags = [ + ['d', 'faust-part-one'], + ['title', 'Faust: Der Tragödie erster Teil'], + ['author', 'Johann Wolfgang von Goethe'], + ['a', `30041:${PK}:intro`] + ] + + expect(publicationMetadataTagMatchesQuery(root, 'author', 'goethe')).toBe(true) + expect(publicationMetadataTagMatchesQuery(root, 'title', 'tragödie')).toBe(true) + expect(publicationMetadataTagMatchesQuery(root, 'd', 'faust')).toBe(false) + expect(publicationMetadataTagMatchesQuery(root, 'd', 'faust-part-one')).toBe(true) }) it('searchLibraryPublications respects author axis and keeps separate cache keys', async () => { diff --git a/src/lib/library-publication-index.ts b/src/lib/library-publication-index.ts index dcde1441..dbbdbb7e 100644 --- a/src/lib/library-publication-index.ts +++ b/src/lib/library-publication-index.ts @@ -1857,7 +1857,7 @@ export function publicationQueryDTagVariants(query: string): string[] { return [...seen] } -/** Normalized needles for exact publication metadata tag match (d / title / author). */ +/** Normalized needles for publication metadata tag match (d / title / author). */ export function publicationQueryNeedles(query: string): string[] { const raw = normalizeGeneralSearchQuery(query.trim()) if (!raw) return [] @@ -1870,13 +1870,19 @@ export function publicationQueryNeedles(query: string): string[] { return [...new Set([lower, normalized, hyphen].filter(Boolean))] } -function publicationTagValueMatchesNeedles(tagValue: string, needles: string[]): boolean { +function publicationTagValueMatchesNeedles( + tagValue: string, + needles: string[], + exactOnly: boolean +): boolean { const val = tagValue.trim().toLowerCase() const valSpaced = val.replace(/-/g, ' ').replace(/\s+/g, ' ').trim() for (const needle of needles) { - if (val === needle) return true + if (!needle) continue const needleSpaced = needle.replace(/-/g, ' ').replace(/\s+/g, ' ').trim() - if (valSpaced === needleSpaced) return true + if (val === needle || valSpaced === needleSpaced) return true + if (exactOnly || needle.length < 2) continue + if (val.includes(needle) || valSpaced.includes(needleSpaced)) return true } return false } @@ -1888,10 +1894,11 @@ export function publicationMetadataTagMatchesQuery( ): boolean { const needles = publicationQueryNeedles(query) if (needles.length === 0) return false + const exactOnly = tagName === 'd' for (const tag of event.tags ?? []) { if ((tag[0] || '').toLowerCase() !== tagName) continue const value = tag[1]?.trim() - if (value && publicationTagValueMatchesNeedles(value, needles)) return true + if (value && publicationTagValueMatchesNeedles(value, needles, exactOnly)) return true } return false }