Browse Source

detailed search

imwald
Silberengel 7 days ago
parent
commit
0818071b0c
  1. 4
      package-lock.json
  2. 2
      package.json
  3. 381
      src/components/Library/LibrarySearchBar.tsx
  4. 25
      src/hooks/useLibraryPublications.ts
  5. 7
      src/i18n/locales/de.ts
  6. 7
      src/i18n/locales/en.ts
  7. 29
      src/lib/library-publication-index.test.ts
  8. 84
      src/lib/library-publication-index.ts
  9. 4
      src/pages/primary/LibraryPage/index.tsx

4
package-lock.json generated

@ -1,12 +1,12 @@ @@ -1,12 +1,12 @@
{
"name": "imwald",
"version": "23.21.7",
"version": "23.21.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "imwald",
"version": "23.21.7",
"version": "23.21.8",
"license": "MIT",
"dependencies": {
"@asciidoctor/core": "^3.0.4",

2
package.json

@ -1,6 +1,6 @@ @@ -1,6 +1,6 @@
{
"name": "imwald",
"version": "23.21.7",
"version": "23.21.8",
"description": "Imwald — a user-friendly Nostr client focused on relay feed browsing, publications, and relay discovery",
"private": true,
"type": "module",

381
src/components/Library/LibrarySearchBar.tsx

@ -1,13 +1,36 @@ @@ -1,13 +1,36 @@
import SearchInput from '@/components/SearchInput'
import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Loader2, Search, Wifi } from 'lucide-react'
import { normalizeToDTag } from '@/lib/search-parser'
import type { LibraryPublicationRelaySearchAxis } from '@/lib/library-publication-index'
import { cn } from '@/lib/utils'
import { useScreenSize } from '@/providers/ScreenSizeProvider'
import modalManager from '@/services/modal-manager.service'
import { randomString } from '@/lib/random'
import { FileText, Loader2, Search, User, Wifi } from 'lucide-react'
import {
HTMLAttributes,
useCallback,
useEffect,
useLayoutEffect,
useMemo,
useRef,
useState
} from 'react'
import { useTranslation } from 'react-i18next'
type LibrarySearchOption = {
axis: LibraryPublicationRelaySearchAxis | null
search: string
input?: string
}
export default function LibrarySearchBar({
searchQuery,
onSearchQueryChange,
searchAxis,
onSearchAxisChange,
showOnlyMine,
onShowOnlyMineChange,
mineFilterLoading,
@ -17,6 +40,8 @@ export default function LibrarySearchBar({ @@ -17,6 +40,8 @@ export default function LibrarySearchBar({
}: {
searchQuery: string
onSearchQueryChange: (value: string) => void
searchAxis: LibraryPublicationRelaySearchAxis | null
onSearchAxisChange: (axis: LibraryPublicationRelaySearchAxis | null) => void
showOnlyMine: boolean
onShowOnlyMineChange: (value: boolean) => void
mineFilterLoading?: boolean
@ -25,22 +50,252 @@ export default function LibrarySearchBar({ @@ -25,22 +50,252 @@ export default function LibrarySearchBar({
disabled?: boolean
}) {
const { t } = useTranslation()
const { isSmallScreen } = useScreenSize()
const [searching, setSearching] = useState(false)
const [displayList, setDisplayList] = useState(false)
const [selectableOptions, setSelectableOptions] = useState<LibrarySearchOption[]>([])
const [selectedIndex, setSelectedIndex] = useState(-1)
const prevSelectableCountRef = useRef(0)
const searchInputRef = useRef<HTMLInputElement>(null)
const barContainerRef = useRef<HTMLDivElement>(null)
const [suggestPanelTop, setSuggestPanelTop] = useState(0)
const id = useMemo(() => `library-search-${randomString()}`, [])
const canSearchRelays = searchQuery.trim().length > 0 && !relaySearchLoading
useEffect(() => {
const search = searchQuery.trim()
if (!search) {
setSelectableOptions([])
setSelectedIndex(-1)
setSearching(false)
return
}
const normalizedDTag = normalizeToDTag(search)
const options: LibrarySearchOption[] = [
{ axis: null, search },
{ axis: 'title', search },
{ axis: 'author', search },
...(normalizedDTag
? [{ axis: 'd-tag' as const, search: normalizedDTag, input: search }]
: [])
]
setSelectableOptions(options)
}, [searchQuery])
useEffect(() => {
setDisplayList(searching && !!searchQuery.trim())
}, [searching, searchQuery])
useEffect(() => {
const trimmed = searchQuery.trim()
const len = selectableOptions.length
if (!trimmed) {
prevSelectableCountRef.current = 0
return
}
if (len > 0 && prevSelectableCountRef.current === 0) {
const el = searchInputRef.current
if (el && document.activeElement !== el) {
queueMicrotask(() => {
el.focus({ preventScroll: true })
})
}
}
prevSelectableCountRef.current = len
}, [searchQuery, selectableOptions])
useEffect(() => {
if (displayList && selectableOptions.length > 0) {
modalManager.register(id, () => {
setDisplayList(false)
})
} else {
modalManager.unregister(id)
}
}, [displayList, selectableOptions.length, id])
const blur = () => {
setSearching(false)
searchInputRef.current?.blur()
}
const applyOption = (option: LibrarySearchOption) => {
onSearchAxisChange(option.axis)
if (option.input && option.input !== searchQuery) {
onSearchQueryChange(option.input)
}
blur()
}
const updateSuggestPanelGeometry = useCallback(() => {
const el = barContainerRef.current
if (!el) return
setSuggestPanelTop(el.getBoundingClientRect().bottom)
}, [])
useLayoutEffect(() => {
if (!displayList || selectableOptions.length === 0 || !isSmallScreen) return
updateSuggestPanelGeometry()
const onScrollOrResize = () => updateSuggestPanelGeometry()
window.addEventListener('scroll', onScrollOrResize, true)
window.addEventListener('resize', onScrollOrResize)
return () => {
window.removeEventListener('scroll', onScrollOrResize, true)
window.removeEventListener('resize', onScrollOrResize)
}
}, [displayList, selectableOptions.length, isSmallScreen, searchQuery, updateSuggestPanelGeometry])
const handleKeyDown = useCallback(
(e: React.KeyboardEvent) => {
if (e.key === 'Enter') {
e.stopPropagation()
if (selectableOptions.length <= 0) return
applyOption(selectableOptions[selectedIndex >= 0 ? selectedIndex : 0])
return
}
if (e.key === 'ArrowDown') {
e.preventDefault()
if (selectableOptions.length <= 0) return
setSelectedIndex((prev) => (prev + 1) % selectableOptions.length)
return
}
if (e.key === 'ArrowUp') {
e.preventDefault()
if (selectableOptions.length <= 0) return
setSelectedIndex((prev) => (prev - 1 + selectableOptions.length) % selectableOptions.length)
return
}
if (e.key === 'Escape') {
blur()
}
},
[selectableOptions, selectedIndex]
)
const list = useMemo(() => {
if (selectableOptions.length <= 0) return null
return (
<>
{selectableOptions.map((option, index) => {
if (option.axis === null) {
return (
<AllFieldsItem
key="all"
search={option.search}
selected={selectedIndex === index}
onClick={() => applyOption(option)}
/>
)
}
if (option.axis === 'title') {
return (
<TitleItem
key="title"
search={option.search}
selected={selectedIndex === index}
onClick={() => applyOption(option)}
/>
)
}
if (option.axis === 'author') {
return (
<AuthorItem
key="author"
search={option.search}
selected={selectedIndex === index}
onClick={() => applyOption(option)}
/>
)
}
return (
<DTagItem
key="dtag"
dtag={option.search}
selected={selectedIndex === index}
onClick={() => applyOption(option)}
/>
)
})}
</>
)
}, [selectableOptions, selectedIndex])
const suggestTopPx = Math.max(0, suggestPanelTop - 4)
const suggestionsPanel = list ? (
<div
className={cn(
'bg-surface-background shadow-lg',
isSmallScreen
? 'fixed left-4 right-4 z-[110] overflow-y-auto rounded-b-lg border border-t-0 border-border/80 pt-1'
: 'absolute top-full z-50 -translate-y-1 inset-x-0 rounded-b-lg pt-1'
)}
style={
isSmallScreen
? {
top: suggestTopPx,
maxHeight: `calc(100dvh - ${suggestTopPx}px - 3.25rem - env(safe-area-inset-bottom, 0px))`
}
: undefined
}
onMouseDown={(e) => e.preventDefault()}
>
<div className="h-fit">{list}</div>
</div>
) : null
const scopeLabel =
searchAxis === 'title'
? t('Library search scope title')
: searchAxis === 'author'
? t('Library search scope author')
: searchAxis === 'd-tag'
? t('Library search scope dtag')
: null
return (
<div className="space-y-3">
<div className="relative">
<Search className="pointer-events-none absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
<div ref={barContainerRef} className="relative">
{displayList && list && !isSmallScreen && (
<>
{suggestionsPanel}
<div className="fixed inset-0 z-40 w-full h-full" onClick={() => blur()} aria-hidden />
</>
)}
{displayList && list && isSmallScreen && (
<>
<div className="fixed inset-0 z-[100] w-full h-full" onClick={() => blur()} aria-hidden />
{suggestionsPanel}
</>
)}
<SearchInput
ref={searchInputRef}
type="search"
value={searchQuery}
onChange={(e) => onSearchQueryChange(e.target.value)}
onChange={(e) => {
setSearching(true)
onSearchQueryChange(e.target.value)
}}
onPaste={() => setSearching(true)}
onKeyDown={handleKeyDown}
onFocus={() => setSearching(true)}
onBlur={() => setSearching(false)}
placeholder={t('Library search placeholder')}
className="pl-9"
className={cn(
'bg-surface-background pl-3',
displayList && isSmallScreen && 'relative z-[120]',
displayList && !isSmallScreen && 'z-50'
)}
disabled={disabled}
aria-label={t('Library search placeholder')}
/>
</div>
{scopeLabel ? (
<p className="text-xs text-muted-foreground">{scopeLabel}</p>
) : null}
{onSearchRelays ? (
<Button
type="button"
@ -75,3 +330,115 @@ export default function LibrarySearchBar({ @@ -75,3 +330,115 @@ export default function LibrarySearchBar({
</div>
)
}
function Item({
className,
children,
selected,
...props
}: HTMLAttributes<HTMLDivElement> & { selected?: boolean }) {
return (
<div
className={cn(
'flex gap-2 items-center px-2 py-3 hover:bg-accent rounded-md cursor-pointer',
selected ? 'bg-accent' : '',
className
)}
{...props}
>
{children}
</div>
)
}
function AllFieldsItem({
search,
onClick,
selected
}: {
search: string
onClick?: () => void
selected?: boolean
}) {
const { t } = useTranslation()
return (
<Item onClick={onClick} selected={selected}>
<div className="flex flex-col items-center gap-0.5">
<Search className="text-muted-foreground" />
<span className="text-[10px] text-muted-foreground/70 uppercase leading-none">
{t('Library search dropdown all')}
</span>
</div>
<div className="font-semibold truncate">{search}</div>
</Item>
)
}
function TitleItem({
search,
onClick,
selected
}: {
search: string
onClick?: () => void
selected?: boolean
}) {
const { t } = useTranslation()
return (
<Item onClick={onClick} selected={selected}>
<div className="flex flex-col items-center gap-0.5">
<FileText className="text-muted-foreground" />
<span className="text-[10px] text-muted-foreground/70 uppercase leading-none">
{t('Library search dropdown title')}
</span>
</div>
<div className="font-semibold truncate">{search}</div>
</Item>
)
}
function AuthorItem({
search,
onClick,
selected
}: {
search: string
onClick?: () => void
selected?: boolean
}) {
const { t } = useTranslation()
return (
<Item onClick={onClick} selected={selected}>
<div className="flex flex-col items-center gap-0.5">
<User className="text-muted-foreground" />
<span className="text-[10px] text-muted-foreground/70 uppercase leading-none">
{t('Library search dropdown author')}
</span>
</div>
<div className="font-semibold truncate">{search}</div>
</Item>
)
}
function DTagItem({
dtag,
onClick,
selected
}: {
dtag: string
onClick?: () => void
selected?: boolean
}) {
const { t } = useTranslation()
return (
<Item onClick={onClick} selected={selected}>
<div className="flex flex-col items-center gap-0.5">
<FileText className="text-muted-foreground" />
<span className="text-[10px] text-muted-foreground/70 uppercase leading-none">
{t('Library search dropdown dtag')}
</span>
</div>
<div className="font-semibold truncate">{dtag}</div>
</Item>
)
}

25
src/hooks/useLibraryPublications.ts

@ -10,6 +10,7 @@ import { @@ -10,6 +10,7 @@ import {
searchLibraryPublications,
searchLibraryPublicationsOnRelays,
type LibraryPublicationEntry,
type LibraryPublicationRelaySearchAxis,
type PublicationEngagementMaps,
type LibraryMineFilterOpts
} from '@/lib/library-publication-index'
@ -59,6 +60,7 @@ export function useLibraryPublications(isActive: boolean) { @@ -59,6 +60,7 @@ export function useLibraryPublications(isActive: boolean) {
const [indexEvents, setIndexEvents] = useState<Event[]>([])
const [engagement, setEngagement] = useState<PublicationEngagementMaps>(EMPTY_ENGAGEMENT)
const [searchQuery, setSearchQuery] = useState('')
const [searchAxis, setSearchAxis] = useState<LibraryPublicationRelaySearchAxis | null>(null)
const [debouncedSearch, setDebouncedSearch] = useState('')
const [showOnlyMine, setShowOnlyMine] = useState(false)
const [loading, setLoading] = useState(false)
@ -134,9 +136,15 @@ export function useLibraryPublications(isActive: boolean) { @@ -134,9 +136,15 @@ export function useLibraryPublications(isActive: boolean) {
return () => window.clearTimeout(t)
}, [searchQuery])
useEffect(() => {
if (!searchQuery.trim()) {
setSearchAxis(null)
}
}, [searchQuery])
useEffect(() => {
setFeedPageIndex(0)
}, [debouncedSearch, showOnlyMine])
}, [debouncedSearch, showOnlyMine, searchAxis])
const applyDefaultFeedSlice = useCallback(
(indexEventsSlice: Event[], engagementMaps: PublicationEngagementMaps, pageIndex: number) => {
@ -273,7 +281,7 @@ export function useLibraryPublications(isActive: boolean) { @@ -273,7 +281,7 @@ export function useLibraryPublications(isActive: boolean) {
return
}
const cached = peekLibrarySearchResults(q, { indexEvents, engagement })
const cached = peekLibrarySearchResults(q, { indexEvents, engagement }, searchAxis)
if (cached) {
setSearchResults(cached)
setSearchLoading(false)
@ -282,7 +290,7 @@ export function useLibraryPublications(isActive: boolean) { @@ -282,7 +290,7 @@ export function useLibraryPublications(isActive: boolean) {
let cancelled = false
setSearchLoading(true)
void searchLibraryPublications(q, { indexEvents, engagement }).then((results) => {
void searchLibraryPublications(q, { indexEvents, engagement }, searchAxis).then((results) => {
if (cancelled) return
setSearchResults(results)
setSearchLoading(false)
@ -291,7 +299,7 @@ export function useLibraryPublications(isActive: boolean) { @@ -291,7 +299,7 @@ export function useLibraryPublications(isActive: boolean) {
return () => {
cancelled = true
}
}, [debouncedSearch, indexEvents, engagement])
}, [debouncedSearch, indexEvents, engagement, searchAxis])
const searchOnRelays = useCallback(async () => {
const q = searchQuery.trim()
@ -303,7 +311,8 @@ export function useLibraryPublications(isActive: boolean) { @@ -303,7 +311,8 @@ export function useLibraryPublications(isActive: boolean) {
const { events, mergedIndexEvents, fromCache } = await searchLibraryPublicationsOnRelays(
q,
relays,
{ indexEvents, engagement }
{ indexEvents, engagement },
{ axis: searchAxis }
)
setIndexEvents(mergedIndexEvents)
setAllIndexCount(mergedIndexEvents.length)
@ -325,7 +334,7 @@ export function useLibraryPublications(isActive: boolean) { @@ -325,7 +334,7 @@ export function useLibraryPublications(isActive: boolean) {
const entries = await searchLibraryPublications(q, {
indexEvents: mergedIndexEvents,
engagement: nextEngagement
})
}, searchAxis)
setSearchResults(entries)
} catch (e) {
const message = e instanceof Error ? e.message : 'Relay search failed'
@ -336,7 +345,7 @@ export function useLibraryPublications(isActive: boolean) { @@ -336,7 +345,7 @@ export function useLibraryPublications(isActive: boolean) {
} finally {
setRelaySearchLoading(false)
}
}, [searchQuery, pubkey, indexEvents, engagement, blockedRelays])
}, [searchQuery, searchAxis, pubkey, indexEvents, engagement, blockedRelays])
const mineFilterOpts = useMemo(
() => ({
@ -435,6 +444,8 @@ export function useLibraryPublications(isActive: boolean) { @@ -435,6 +444,8 @@ export function useLibraryPublications(isActive: boolean) {
entries: filteredEntries,
searchQuery,
setSearchQuery,
searchAxis,
setSearchAxis,
showOnlyMine,
setShowOnlyMine,
mineFilterLoading:

7
src/i18n/locales/de.ts

@ -1650,6 +1650,13 @@ export default { @@ -1650,6 +1650,13 @@ export default {
Library: 'Bibliothek',
'Library page title': 'Bibliothek',
'Library search placeholder': 'Publikationen nach Titel, Autor, Quelle, Tag oder Abschnitt suchen…',
'Library search dropdown all': 'ALLE FELDER',
'Library search dropdown title': 'TITEL',
'Library search dropdown author': 'AUTOR',
'Library search dropdown dtag': 'D-TAG',
'Library search scope title': 'Suche nach Titel',
'Library search scope author': 'Suche nach Autor',
'Library search scope dtag': 'Suche nach d-Tag',
'Library show only my publications': 'Meine Publikationen',
'Library empty': 'Noch keine Publikationen auf deinen Relays gefunden.',
'Library empty filtered': 'Keine Publikationen entsprechen den Filtern.',

7
src/i18n/locales/en.ts

@ -1673,6 +1673,13 @@ export default { @@ -1673,6 +1673,13 @@ export default {
Library: 'Library',
'Library page title': 'Library',
'Library search placeholder': 'Search publications by title, author, source, tag, or section…',
'Library search dropdown all': 'ALL FIELDS',
'Library search dropdown title': 'TITLE',
'Library search dropdown author': 'AUTHOR',
'Library search dropdown dtag': 'D-TAG',
'Library search scope title': 'Searching by title',
'Library search scope author': 'Searching by author',
'Library search scope dtag': 'Searching by d-tag',
'Library show only my publications': 'My publications',
'Library empty': 'No publications found on your relays yet.',
'Library empty filtered': 'No publications match your filters.',

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

@ -285,6 +285,35 @@ describe('library-publication-index', () => { @@ -285,6 +285,35 @@ describe('library-publication-index', () => {
expect(publicationMetadataTagMatchesQuery(root, 'title', 'Jane Eyre')).toBe(true)
})
it('searchLibraryPublications respects author axis and keeps separate cache keys', async () => {
clearLibrarySearchSessionCache()
const about = indexEvent('about-aristotle', [`30041:${PK}:intro`])
about.tags = [
['d', 'about-aristotle'],
['title', 'Aristotle: A Very Short Introduction'],
['author', 'John Smith'],
['a', `30041:${PK}:intro`]
]
const fromAuthor = indexEvent('nicomachean-ethics', [`30041:${PK}:ch`])
fromAuthor.tags = [
['d', 'nicomachean-ethics'],
['title', 'Nicomachean Ethics'],
['author', 'Aristotle'],
['a', `30041:${PK}:ch`]
]
const indexEvents = [about, fromAuthor]
const engagement = buildEngagementMapsFromEvents([], [], [])
const broad = await searchLibraryPublications('aristotle', { indexEvents, engagement })
expect(broad.map((e) => e.event.id).sort()).toEqual([about.id, fromAuthor.id].sort())
const byAuthor = await searchLibraryPublications('aristotle', { indexEvents, engagement }, 'author')
expect(byAuthor.map((e) => e.event.id)).toEqual([fromAuthor.id])
expect(peekLibrarySearchResults('aristotle', { indexEvents, engagement }, 'author')).toHaveLength(1)
expect(peekLibrarySearchResults('aristotle', { indexEvents, engagement })).toHaveLength(2)
})
it('searchLibraryPublications caches results for repeated queries', async () => {
clearLibrarySearchSessionCache()
const root = indexEvent('book', [`30041:${PK}:intro`])

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

@ -200,8 +200,13 @@ type LibrarySearchSessionRow = { @@ -200,8 +200,13 @@ type LibrarySearchSessionRow = {
const librarySearchSessionCache = new Map<string, LibrarySearchSessionRow>()
function librarySearchQueryKey(query: string): string {
return normalizeGeneralSearchQuery(query).toLowerCase()
function librarySearchQueryKey(
query: string,
axis?: LibraryPublicationRelaySearchAxis | null
): string {
const base = normalizeGeneralSearchQuery(query).toLowerCase()
if (!axis) return base
return `${axis}:${base}`
}
function librarySearchFingerprint(context: LibrarySearchContext): string {
@ -232,9 +237,9 @@ function librarySearchFingerprint(context: LibrarySearchContext): string { @@ -232,9 +237,9 @@ function librarySearchFingerprint(context: LibrarySearchContext): string {
function getLibrarySearchSessionRow(
query: string,
context: LibrarySearchContext,
opts?: { requireRelaySearch?: boolean }
opts?: { requireRelaySearch?: boolean; axis?: LibraryPublicationRelaySearchAxis | null }
): LibrarySearchSessionRow | null {
const key = librarySearchQueryKey(query)
const key = librarySearchQueryKey(query, opts?.axis)
if (!key) return null
const row = librarySearchSessionCache.get(key)
if (!row) return null
@ -246,9 +251,10 @@ function getLibrarySearchSessionRow( @@ -246,9 +251,10 @@ function getLibrarySearchSessionRow(
function putLibrarySearchSessionRow(
query: string,
context: LibrarySearchContext,
row: Omit<LibrarySearchSessionRow, 'fingerprint'>
row: Omit<LibrarySearchSessionRow, 'fingerprint'>,
axis?: LibraryPublicationRelaySearchAxis | null
): void {
const key = librarySearchQueryKey(query)
const key = librarySearchQueryKey(query, axis)
if (!key) return
librarySearchSessionCache.set(key, {
...row,
@ -259,9 +265,10 @@ function putLibrarySearchSessionRow( @@ -259,9 +265,10 @@ function putLibrarySearchSessionRow(
/** Sync read of cached search hits for the current index + engagement snapshot. */
export function peekLibrarySearchResults(
query: string,
context: LibrarySearchContext
context: LibrarySearchContext,
axis?: LibraryPublicationRelaySearchAxis | null
): LibraryPublicationEntry[] | null {
return getLibrarySearchSessionRow(query, context)?.entries ?? null
return getLibrarySearchSessionRow(query, context, { axis })?.entries ?? null
}
export function clearLibrarySearchSessionCache(): void {
@ -1595,7 +1602,8 @@ export async function refreshLibraryEngagement( @@ -1595,7 +1602,8 @@ export async function refreshLibraryEngagement(
export function searchLibraryPublicationIndex(
query: string,
indexEvents: Event[],
indexByAddress: Map<string, Event>
indexByAddress: Map<string, Event>,
axis?: LibraryPublicationRelaySearchAxis | null
): Event[] {
const q = query.trim()
if (!q || indexEvents.length === 0) return []
@ -1607,7 +1615,7 @@ export function searchLibraryPublicationIndex( @@ -1607,7 +1615,7 @@ export function searchLibraryPublicationIndex(
for (const ev of indexEvents) {
if (ev.kind !== ExtendedKind.PUBLICATION) continue
if (!publicationIndexMatchesSearchQuery(ev, q)) continue
if (!publicationIndexMatchesSearchQueryWithAxis(ev, q, axis)) continue
if (topLevelIds.has(ev.id)) {
roots.set(ev.id, ev)
@ -1633,15 +1641,20 @@ export type LibrarySearchContext = { @@ -1633,15 +1641,20 @@ export type LibrarySearchContext = {
*/
export async function searchLibraryPublications(
query: string,
context: LibrarySearchContext
context: LibrarySearchContext,
axis?: LibraryPublicationRelaySearchAxis | null
): Promise<LibraryPublicationEntry[]> {
const q = query.trim()
if (!q) return []
const cached = getLibrarySearchSessionRow(q, context)
const cached = getLibrarySearchSessionRow(q, context, { axis })
if (cached) {
if (import.meta.env.DEV) {
logger.info('[Library] search cache hit', { query: q, relaySearched: cached.relaySearched })
logger.info('[Library] search cache hit', {
query: q,
axis: axis ?? 'all',
relaySearched: cached.relaySearched
})
}
return cached.entries
}
@ -1653,7 +1666,7 @@ export async function searchLibraryPublications( @@ -1653,7 +1666,7 @@ export async function searchLibraryPublications(
const engagement = context.engagement ?? EMPTY_ENGAGEMENT
const indexByAddress = buildIndexByAddress(indexEvents)
const fromIndex = searchLibraryPublicationIndex(q, indexEvents, indexByAddress)
const fromIndex = searchLibraryPublicationIndex(q, indexEvents, indexByAddress, axis)
const rootMap = new Map<string, Event>()
for (const root of fromIndex) rootMap.set(root.id, root)
@ -1669,7 +1682,7 @@ export async function searchLibraryPublications( @@ -1669,7 +1682,7 @@ export async function searchLibraryPublications(
)
for (const ev of fromReadingCache) {
if (ev.kind !== ExtendedKind.PUBLICATION) continue
if (!publicationIndexMatchesSearchQuery(ev, q)) continue
if (!publicationIndexMatchesSearchQueryWithAxis(ev, q, axis)) continue
if (rootMap.has(ev.id)) continue
const addr = eventTagAddress(ev)
@ -1696,12 +1709,17 @@ export async function searchLibraryPublications( @@ -1696,12 +1709,17 @@ export async function searchLibraryPublications(
const entries = sortLibraryPublications(libraryEntriesFromRoots(roots, indexByAddress, engagement))
const searchContext: LibrarySearchContext = { indexEvents, engagement }
const prev = getLibrarySearchSessionRow(q, searchContext)
putLibrarySearchSessionRow(q, searchContext, {
const prev = getLibrarySearchSessionRow(q, searchContext, { axis })
putLibrarySearchSessionRow(
q,
searchContext,
{
entries,
mergedIndexEvents: prev?.mergedIndexEvents ?? indexEvents,
relaySearched: prev?.relaySearched ?? false
})
},
axis
)
return entries
}
@ -1920,6 +1938,15 @@ export function filterEventsForPublicationRelaySearchAxis( @@ -1920,6 +1938,15 @@ export function filterEventsForPublicationRelaySearchAxis(
})
}
export function publicationIndexMatchesSearchQueryWithAxis(
event: Event,
query: string,
axis?: LibraryPublicationRelaySearchAxis | null
): boolean {
if (!axis) return publicationIndexMatchesSearchQuery(event, query)
return filterEventsForPublicationRelaySearchAxis([event], axis, query).length > 0
}
async function scanHttpIndexRelayForPublicationAxis(
httpRelay: string,
axis: LibraryPublicationRelaySearchAxis,
@ -2007,7 +2034,7 @@ export async function searchLibraryPublicationsOnRelays( @@ -2007,7 +2034,7 @@ export async function searchLibraryPublicationsOnRelays(
query: string,
relayUrls: string[],
context: LibrarySearchContext,
options?: { forceRefresh?: boolean }
options?: { forceRefresh?: boolean; axis?: LibraryPublicationRelaySearchAxis | null }
): Promise<{
events: Event[]
entries: LibraryPublicationEntry[]
@ -2020,7 +2047,10 @@ export async function searchLibraryPublicationsOnRelays( @@ -2020,7 +2047,10 @@ export async function searchLibraryPublicationsOnRelays(
}
if (!options?.forceRefresh) {
const cached = getLibrarySearchSessionRow(q, context, { requireRelaySearch: true })
const cached = getLibrarySearchSessionRow(q, context, {
requireRelaySearch: true,
axis: options?.axis
})
if (cached) {
if (import.meta.env.DEV) {
logger.info('[Library] relay search cache hit', { query: q })
@ -2038,8 +2068,9 @@ export async function searchLibraryPublicationsOnRelays( @@ -2038,8 +2068,9 @@ export async function searchLibraryPublicationsOnRelays(
const { wsRelays, httpRelays } = splitWsAndHttpRelays(indexRelays)
const batches: Promise<Event[]>[] = []
let filterCount = 0
const axes = options?.axis ? [options.axis] : LIBRARY_PUBLICATION_RELAY_SEARCH_AXES
for (const axis of LIBRARY_PUBLICATION_RELAY_SEARCH_AXES) {
for (const axis of axes) {
const npubQuery = tryNpubFromQuery(q)
if (npubQuery && axis !== 'author') continue
@ -2125,7 +2156,7 @@ export async function searchLibraryPublicationsOnRelays( @@ -2125,7 +2156,7 @@ export async function searchLibraryPublicationsOnRelays(
void persistLibraryIndexCacheEvents(mergedIndex)
}
const indexByAddress = buildIndexByAddress(mergedIndex)
const roots = searchLibraryPublicationIndex(q, mergedIndex, indexByAddress)
const roots = searchLibraryPublicationIndex(q, mergedIndex, indexByAddress, options?.axis)
const engagement = context.engagement ?? EMPTY_ENGAGEMENT
const entries = sortLibraryPublications(
libraryEntriesFromRoots(roots, indexByAddress, engagement)
@ -2135,11 +2166,16 @@ export async function searchLibraryPublicationsOnRelays( @@ -2135,11 +2166,16 @@ export async function searchLibraryPublicationsOnRelays(
indexEvents: mergedIndex,
engagement
}
putLibrarySearchSessionRow(q, searchContext, {
putLibrarySearchSessionRow(
q,
searchContext,
{
entries,
mergedIndexEvents: mergedIndex,
relaySearched: true
})
},
options?.axis
)
if (import.meta.env.DEV) {
logger.info('[Library] relay search done', {

4
src/pages/primary/LibraryPage/index.tsx

@ -20,6 +20,8 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => { @@ -20,6 +20,8 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => {
entries,
searchQuery,
setSearchQuery,
searchAxis,
setSearchAxis,
showOnlyMine,
setShowOnlyMine,
mineFilterLoading,
@ -68,6 +70,8 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => { @@ -68,6 +70,8 @@ const LibraryPage = forwardRef<TPageRef>((_props, ref) => {
<LibrarySearchBar
searchQuery={searchQuery}
onSearchQueryChange={setSearchQuery}
searchAxis={searchAxis}
onSearchAxisChange={setSearchAxis}
showOnlyMine={showOnlyMine}
onShowOnlyMineChange={setShowOnlyMine}
mineFilterLoading={mineFilterLoading}

Loading…
Cancel
Save