feat(events): include organizer name in event fuzzy search
Search only indexed title/summary/description/location/tags, so typing an organizer's name found nothing. Organizer display names aren't stored on the event (they're resolved per-pubkey into the shared ProfileService cache), so enrich the search corpus with the resolved name read from that same reactive cache and add it as a Fuse key. Opening the search overlay warms the cache for any organizers not yet fetched, and the corpus recomputes as kind-0 metadata arrives. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
a5e5136591
commit
3189adf579
1 changed files with 34 additions and 3 deletions
|
|
@ -7,8 +7,13 @@ import { Input } from '@/components/ui/input'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Search, X, MapPin, Calendar } from 'lucide-vue-next'
|
import { Search, X, MapPin, Calendar } from 'lucide-vue-next'
|
||||||
import { useFuzzySearch, type FuzzySearchOptions } from '@/composables/useFuzzySearch'
|
import { useFuzzySearch, type FuzzySearchOptions } from '@/composables/useFuzzySearch'
|
||||||
|
import { tryInjectService, SERVICE_TOKENS } from '@/core/di-container'
|
||||||
|
import type { ProfileService } from '@/modules/base/nostr/ProfileService'
|
||||||
import type { Event } from '../types/event'
|
import type { Event } from '../types/event'
|
||||||
|
|
||||||
|
/** Event enriched with its resolved organizer display name for search. */
|
||||||
|
type SearchableEvent = Event & { organizerName: string }
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
events: Event[]
|
events: Event[]
|
||||||
}>()
|
}>()
|
||||||
|
|
@ -22,12 +27,13 @@ const { dateLocale } = useDateLocale()
|
||||||
const isOpen = ref(false)
|
const isOpen = ref(false)
|
||||||
const inputRef = ref<HTMLInputElement | null>(null)
|
const inputRef = ref<HTMLInputElement | null>(null)
|
||||||
|
|
||||||
const searchOptions: FuzzySearchOptions<Event> = {
|
const searchOptions: FuzzySearchOptions<SearchableEvent> = {
|
||||||
fuseOptions: {
|
fuseOptions: {
|
||||||
keys: [
|
keys: [
|
||||||
{ name: 'title', weight: 0.5 },
|
{ name: 'title', weight: 0.5 },
|
||||||
{ name: 'summary', weight: 0.2 },
|
{ name: 'summary', weight: 0.2 },
|
||||||
{ name: 'description', weight: 0.15 },
|
{ name: 'description', weight: 0.15 },
|
||||||
|
{ name: 'organizerName', weight: 0.1 },
|
||||||
{ name: 'location', weight: 0.1 },
|
{ name: 'location', weight: 0.1 },
|
||||||
{ name: 'tags', weight: 0.05 },
|
{ name: 'tags', weight: 0.05 },
|
||||||
],
|
],
|
||||||
|
|
@ -39,7 +45,20 @@ const searchOptions: FuzzySearchOptions<Event> = {
|
||||||
resultLimit: 8,
|
resultLimit: 8,
|
||||||
}
|
}
|
||||||
|
|
||||||
const eventsRef = computed(() => props.events)
|
// Organizer display names aren't stored on the event (they're fetched
|
||||||
|
// per-pubkey into the shared ProfileService cache). Read the resolved
|
||||||
|
// name from that same reactive cache so search matches it; the corpus
|
||||||
|
// recomputes as kind-0 metadata lands.
|
||||||
|
const profileService = tryInjectService<ProfileService>(SERVICE_TOKENS.PROFILE_SERVICE)
|
||||||
|
|
||||||
|
function organizerNameFor(pubkey: string): string {
|
||||||
|
const p = profileService?.profiles.get(pubkey)
|
||||||
|
return p?.display_name ?? p?.name ?? ''
|
||||||
|
}
|
||||||
|
|
||||||
|
const searchCorpus = computed<SearchableEvent[]>(() =>
|
||||||
|
props.events.map((e) => ({ ...e, organizerName: organizerNameFor(e.organizer.pubkey) })),
|
||||||
|
)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
searchQuery,
|
searchQuery,
|
||||||
|
|
@ -47,7 +66,7 @@ const {
|
||||||
isSearching,
|
isSearching,
|
||||||
clearSearch,
|
clearSearch,
|
||||||
setSearchQuery,
|
setSearchQuery,
|
||||||
} = useFuzzySearch(eventsRef, searchOptions)
|
} = useFuzzySearch(searchCorpus, searchOptions)
|
||||||
|
|
||||||
const showResults = computed(() => isOpen.value && isSearching.value && filteredItems.value.length > 0)
|
const showResults = computed(() => isOpen.value && isSearching.value && filteredItems.value.length > 0)
|
||||||
const showNoResults = computed(() => isOpen.value && isSearching.value && filteredItems.value.length === 0)
|
const showNoResults = computed(() => isOpen.value && isSearching.value && filteredItems.value.length === 0)
|
||||||
|
|
@ -94,6 +113,18 @@ function handleClickOutside(e: MouseEvent) {
|
||||||
watch(isOpen, (open) => {
|
watch(isOpen, (open) => {
|
||||||
if (open) {
|
if (open) {
|
||||||
document.addEventListener('click', handleClickOutside)
|
document.addEventListener('click', handleClickOutside)
|
||||||
|
// Warm the shared profile cache for every organizer in the current
|
||||||
|
// set so their names become searchable (fetches dedupe in the
|
||||||
|
// service; the corpus reacts as kind-0 metadata arrives).
|
||||||
|
if (profileService) {
|
||||||
|
const seen = new Set<string>()
|
||||||
|
for (const e of props.events) {
|
||||||
|
const pk = e.organizer.pubkey
|
||||||
|
if (seen.has(pk) || profileService.profiles.get(pk)) continue
|
||||||
|
seen.add(pk)
|
||||||
|
void profileService.getProfile(pk)
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
document.removeEventListener('click', handleClickOutside)
|
document.removeEventListener('click', handleClickOutside)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue