Browse Source

updated search, to be more precise

imwald
Silberengel 4 months ago
parent
commit
48ae98f003
  1. 25
      src/components/SearchBar/index.tsx
  2. 15
      src/components/SearchInfo.tsx
  3. 31
      src/lib/search-parser.ts
  4. 24
      src/pages/secondary/NoteListPage/index.tsx
  5. 60
      src/pages/secondary/SearchPage/index.tsx

25
src/components/SearchBar/index.tsx

@ -343,7 +343,10 @@ function NormalItem({
}) { }) {
return ( return (
<Item onClick={onClick} selected={selected}> <Item onClick={onClick} selected={selected}>
<Search className="text-muted-foreground" /> <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">FULL TEXT</span>
</div>
<div className="font-semibold truncate">{search}</div> <div className="font-semibold truncate">{search}</div>
</Item> </Item>
) )
@ -360,7 +363,10 @@ function HashtagItem({
}) { }) {
return ( return (
<Item onClick={onClick} selected={selected}> <Item onClick={onClick} selected={selected}>
<Hash className="text-muted-foreground" /> <div className="flex flex-col items-center gap-0.5">
<Hash className="text-muted-foreground" />
<span className="text-[10px] text-muted-foreground/70 uppercase leading-none">HASHTAG</span>
</div>
<div className="font-semibold truncate">{hashtag}</div> <div className="font-semibold truncate">{hashtag}</div>
</Item> </Item>
) )
@ -377,7 +383,10 @@ function NoteItem({
}) { }) {
return ( return (
<Item onClick={onClick} selected={selected}> <Item onClick={onClick} selected={selected}>
<Notebook className="text-muted-foreground" /> <div className="flex flex-col items-center gap-0.5">
<Notebook className="text-muted-foreground" />
<span className="text-[10px] text-muted-foreground/70 uppercase leading-none">NOTE</span>
</div>
<div className="font-semibold truncate">{id}</div> <div className="font-semibold truncate">{id}</div>
</Item> </Item>
) )
@ -413,7 +422,10 @@ function DTagItem({
}) { }) {
return ( return (
<Item onClick={onClick} selected={selected}> <Item onClick={onClick} selected={selected}>
<FileText className="text-muted-foreground" /> <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">D-TAG</span>
</div>
<div className="font-semibold truncate">{dtag}</div> <div className="font-semibold truncate">{dtag}</div>
</Item> </Item>
) )
@ -430,7 +442,10 @@ function RelayItem({
}) { }) {
return ( return (
<Item onClick={onClick} selected={selected}> <Item onClick={onClick} selected={selected}>
<Server className="text-muted-foreground" /> <div className="flex flex-col items-center gap-0.5">
<Server className="text-muted-foreground" />
<span className="text-[10px] text-muted-foreground/70 uppercase leading-none">RELAY</span>
</div>
<div className="font-semibold truncate">{url}</div> <div className="font-semibold truncate">{url}</div>
</Item> </Item>
) )

15
src/components/SearchInfo.tsx

@ -36,17 +36,13 @@ export default function SearchInfo() {
</ul> </ul>
</div> </div>
<div> <div>
<strong>Metadata fields:</strong> <strong>Filters:</strong>
<ul className="ml-4 mt-1 space-y-1 list-disc"> <ul className="ml-4 mt-1 space-y-1 list-disc">
<li><code className="text-xs">title:"text"</code> or <code className="text-xs">title:text</code> - Search in title tag</li> <li><code className="text-xs">t:hashtag</code> or <code className="text-xs">hashtag:hashtag</code> - Filter by hashtag (t-tag)</li>
<li><code className="text-xs">subject:"text"</code> or <code className="text-xs">subject:text</code> - Search in subject tag</li>
<li><code className="text-xs">description:"text"</code> - Search in description tag</li>
<li><code className="text-xs">author:"name"</code> - Search by author tag (not pubkey)</li>
<li><code className="text-xs">pubkey:npub...</code>, <code className="text-xs">pubkey:hex</code>, <code className="text-xs">pubkey:nprofile...</code>, or <code className="text-xs">pubkey:user@domain.com</code> - Filter by pubkey (accepts npub, nprofile, hex, or NIP-05)</li> <li><code className="text-xs">pubkey:npub...</code>, <code className="text-xs">pubkey:hex</code>, <code className="text-xs">pubkey:nprofile...</code>, or <code className="text-xs">pubkey:user@domain.com</code> - Filter by pubkey (accepts npub, nprofile, hex, or NIP-05)</li>
<li><code className="text-xs">events:hex</code>, <code className="text-xs">events:note1...</code>, <code className="text-xs">events:nevent1...</code>, or <code className="text-xs">events:naddr1...</code> - Filter by specific events (accepts hex, note, nevent, or naddr)</li> <li><code className="text-xs">events:hex</code>, <code className="text-xs">events:note1...</code>, <code className="text-xs">events:nevent1...</code>, or <code className="text-xs">events:naddr1...</code> - Filter by specific events (accepts hex, note, nevent, or naddr)</li>
<li><code className="text-xs">type:value</code> - Filter by type tag</li>
<li><code className="text-xs">kind:30023</code> - Filter by event kind (e.g., 1=notes, 30023=articles, 30817/30818=wiki)</li> <li><code className="text-xs">kind:30023</code> - Filter by event kind (e.g., 1=notes, 30023=articles, 30817/30818=wiki)</li>
<li>Multiple values supported: <code className="text-xs">author:Aristotle,Plato</code> or <code className="text-xs">kind:30023,2018</code></li> <li>Multiple values supported: <code className="text-xs">t:bitcoin,nostr</code>, <code className="text-xs">pubkey:npub1...,npub2...</code> or <code className="text-xs">kind:30023,2018</code></li>
</ul> </ul>
</div> </div>
<div className="pt-2 border-t"> <div className="pt-2 border-t">
@ -55,8 +51,9 @@ export default function SearchInfo() {
</p> </p>
<ul className="ml-4 mt-1 space-y-1 list-disc text-xs text-muted-foreground"> <ul className="ml-4 mt-1 space-y-1 list-disc text-xs text-muted-foreground">
<li><code>jumble search</code> searches d-tag</li> <li><code>jumble search</code> searches d-tag</li>
<li><code>title:"My Article" from:2024-01-01</code></li> <li><code>t:bitcoin from:2024-01-01</code></li>
<li><code>author:"John Doe" type:wiki</code></li> <li><code>pubkey:npub1abc... from:2024-01-01</code></li>
<li><code>kind:30023 from:2024-01-01</code></li>
<li><code>2025-10-23 to 2025-10-30</code> date range</li> <li><code>2025-10-23 to 2025-10-30</code> date range</li>
</ul> </ul>
</div> </div>

31
src/lib/search-parser.ts

@ -2,18 +2,20 @@
* Advanced search parser for Nostr events * Advanced search parser for Nostr events
* Supports multiple search parameters: * Supports multiple search parameters:
* - Date ranges: YYYY-MM-DD to YYYY-MM-DD, from:YYYY-MM-DD, to:YYYY-MM-DD, before:YYYY-MM-DD, after:YYYY-MM-DD * - Date ranges: YYYY-MM-DD to YYYY-MM-DD, from:YYYY-MM-DD, to:YYYY-MM-DD, before:YYYY-MM-DD, after:YYYY-MM-DD
* - Title: title:"text" or title:text * - Hashtag: t:hashtag or hashtag:hashtag (filters by #t tag)
* - Subject: subject:"text" or subject:text * - Pubkey: pubkey:npub... or pubkey:hex... (filters by authors field)
* - Description: description:"text" or description:text * - Events: events:hex, events:note1..., events:nevent1..., events:naddr1... (filters by ids field)
* - Author: author:"name" (author tag, not pubkey)
* - Pubkey: pubkey:npub... or pubkey:hex...
* - Type: type:value
* - Kind: kind:30023 (filter by event kind) * - Kind: kind:30023 (filter by event kind)
* - Plain text: becomes d-tag search for replaceable events * - Plain text: becomes d-tag search for replaceable events (uses #d tag)
*
* Note: Nostr only supports single-letter tag indexes (#d, #t, #p, #e, #a, etc.)
* Multi-letter tags like title, subject, description, author, type are parsed but
* not used in filters as relays don't index them.
*/ */
export interface AdvancedSearchParams { export interface AdvancedSearchParams {
dtag?: string dtag?: string
hashtag?: string | string[] // t-tag/hashtag (uses #t tag)
title?: string | string[] title?: string | string[]
subject?: string | string[] subject?: string | string[]
description?: string | string[] description?: string | string[]
@ -81,8 +83,8 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams {
// Support both 4-digit (YYYY) and 2-digit (YY) years, date ranges (DATE to DATE) // Support both 4-digit (YYYY) and 2-digit (YY) years, date ranges (DATE to DATE)
const dateRangePattern = /(\d{2,4}-\d{2}-\d{2})\s+to\s+(\d{2,4}-\d{2}-\d{2})/gi const dateRangePattern = /(\d{2,4}-\d{2}-\d{2})\s+to\s+(\d{2,4}-\d{2}-\d{2})/gi
const datePattern = /(?:from|to|before|after):(\d{2,4}-\d{2}-\d{2})/gi const datePattern = /(?:from|to|before|after):(\d{2,4}-\d{2}-\d{2})/gi
const quotedPattern = /(title|subject|description|author|type|pubkey|events):"([^"]+)"/gi const quotedPattern = /(title|subject|description|author|type|pubkey|events|hashtag|t):"([^"]+)"/gi
const unquotedPattern = /(title|subject|description|author|pubkey|type|kind|events):([^\s]+)/gi const unquotedPattern = /(title|subject|description|author|pubkey|type|kind|events|hashtag|t):([^\s]+)/gi
// Pattern to detect bare nip19 IDs (nevent, note, naddr) or hex event IDs // Pattern to detect bare nip19 IDs (nevent, note, naddr) or hex event IDs
// These start with the prefix and are base32 encoded (use word boundary to avoid partial matches) // These start with the prefix and are base32 encoded (use word boundary to avoid partial matches)
@ -167,6 +169,10 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams {
const values = parseValues(value) const values = parseValues(value)
switch (param) { switch (param) {
case 'hashtag':
case 't':
params.hashtag = values.length === 1 ? values[0] : values
break
case 'title': case 'title':
params.title = values.length === 1 ? values[0] : values params.title = values.length === 1 ? values[0] : values
break break
@ -209,6 +215,13 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams {
lastIndex = Math.max(lastIndex, end) lastIndex = Math.max(lastIndex, end)
switch (param) { switch (param) {
case 'hashtag':
case 't':
if (!params.hashtag) {
const values = parseValues(value)
params.hashtag = values.length === 1 ? values[0] : values
}
break
case 'title': case 'title':
if (!params.title) { if (!params.title) {
const values = parseValues(value) const values = parseValues(value)

24
src/pages/secondary/NoteListPage/index.tsx

@ -185,29 +185,19 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
} }
// Advanced search parameters (support multiple values) // Advanced search parameters (support multiple values)
const title = searchParams.getAll('title') // Note: Nostr only supports single-letter tag indexes, so we can't filter by
const subject = searchParams.getAll('subject') // multi-letter tags like title, subject, description, author, type
const description = searchParams.getAll('description')
const author = searchParams.getAll('author')
const searchPubkey = searchParams.getAll('pubkey') const searchPubkey = searchParams.getAll('pubkey')
const searchEvents = searchParams.getAll('events') const searchEvents = searchParams.getAll('events')
const type = searchParams.getAll('type')
const from = searchParams.get('from') const from = searchParams.get('from')
const to = searchParams.get('to') const to = searchParams.get('to')
const before = searchParams.get('before') const before = searchParams.get('before')
const after = searchParams.get('after') const after = searchParams.get('after')
// Check if we have any advanced search parameters // Check if we have any advanced search parameters
if (title.length > 0 || subject.length > 0 || description.length > 0 || author.length > 0 || searchPubkey.length > 0 || searchEvents.length > 0 || type.length > 0 || from || to || before || after) { if (searchPubkey.length > 0 || searchEvents.length > 0 || from || to || before || after) {
const filter: any = {} const filter: any = {}
// Tag-based filters (support multiple values - use OR logic)
if (title.length > 0) filter['#title'] = title
if (subject.length > 0) filter['#subject'] = subject
if (description.length > 0) filter['#description'] = description
if (author.length > 0) filter['#author'] = author
if (type.length > 0) filter['#type'] = type
// Pubkey filter (support multiple pubkeys: hex, npub, nprofile, NIP-05) // Pubkey filter (support multiple pubkeys: hex, npub, nprofile, NIP-05)
if (searchPubkey.length > 0) { if (searchPubkey.length > 0) {
const decodedPubkeys: string[] = [] const decodedPubkeys: string[] = []
@ -315,16 +305,16 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
// Build title from search params // Build title from search params
const titleParts: string[] = [] const titleParts: string[] = []
if (title.length > 0) titleParts.push(`title:${title.join(',')}`) // Note: hashtag is handled separately via 't' parameter earlier in the function
if (subject.length > 0) titleParts.push(`subject:${subject.join(',')}`)
if (author.length > 0) titleParts.push(`author:${author.join(',')}`)
if (searchPubkey.length > 0) { if (searchPubkey.length > 0) {
const pubkeyDisplay = searchPubkey.length === 1 const pubkeyDisplay = searchPubkey.length === 1
? `${searchPubkey[0].substring(0, 16)}...` ? `${searchPubkey[0].substring(0, 16)}...`
: `${searchPubkey.length} pubkeys` : `${searchPubkey.length} pubkeys`
titleParts.push(`pubkey:${pubkeyDisplay}`) titleParts.push(`pubkey:${pubkeyDisplay}`)
} }
if (type.length > 0) titleParts.push(`type:${type.join(',')}`) if (searchEvents.length > 0) {
titleParts.push(`${searchEvents.length} event${searchEvents.length > 1 ? 's' : ''}`)
}
if (from || to || before || after) { if (from || to || before || after) {
const dateParts: string[] = [] const dateParts: string[] = []
if (from) dateParts.push(`from:${from}`) if (from) dateParts.push(`from:${from}`)

60
src/pages/secondary/SearchPage/index.tsx

@ -46,44 +46,39 @@ const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number
const searchParams = parseAdvancedSearch(params.search) const searchParams = parseAdvancedSearch(params.search)
// Check if we have advanced search parameters (not just plain text) // Check if we have advanced search parameters (not just plain text)
// Exclude unsupported multi-letter tag params (title, subject, description, author, type)
const hasAdvancedParams = Object.keys(searchParams).some(key => const hasAdvancedParams = Object.keys(searchParams).some(key =>
key !== 'dtag' && searchParams[key as keyof typeof searchParams] key !== 'dtag' &&
key !== 'title' &&
key !== 'subject' &&
key !== 'description' &&
key !== 'author' &&
key !== 'type' &&
searchParams[key as keyof typeof searchParams]
) )
// Handle hashtag search - route to hashtag page
if (searchParams.hashtag) {
const hashtag = Array.isArray(searchParams.hashtag) ? searchParams.hashtag[0] : searchParams.hashtag
const urlParams = new URLSearchParams()
urlParams.set('t', hashtag)
if (searchParams.kinds) {
searchParams.kinds.forEach(k => urlParams.append('k', k.toString()))
}
push(`/notes?${urlParams.toString()}`)
return
}
if (hasAdvancedParams || searchParams.dtag) { if (hasAdvancedParams || searchParams.dtag) {
// Route to NoteListPage with advanced search // Route to NoteListPage with advanced search
// Note: Only include parameters that Nostr relays actually support
// (single-letter tag indexes: #d, #t, #p, #e, #a, etc.)
const urlParams = new URLSearchParams() const urlParams = new URLSearchParams()
if (searchParams.dtag) { if (searchParams.dtag) {
urlParams.set('d', searchParams.dtag) urlParams.set('d', searchParams.dtag)
} }
if (searchParams.title) { // Skip title, subject, description, author, type - these use multi-letter tags
if (Array.isArray(searchParams.title)) { // that Nostr relays don't index
searchParams.title.forEach(t => urlParams.append('title', t))
} else {
urlParams.set('title', searchParams.title)
}
}
if (searchParams.subject) {
if (Array.isArray(searchParams.subject)) {
searchParams.subject.forEach(s => urlParams.append('subject', s))
} else {
urlParams.set('subject', searchParams.subject)
}
}
if (searchParams.description) {
if (Array.isArray(searchParams.description)) {
searchParams.description.forEach(d => urlParams.append('description', d))
} else {
urlParams.set('description', searchParams.description)
}
}
if (searchParams.author) {
if (Array.isArray(searchParams.author)) {
searchParams.author.forEach(a => urlParams.append('author', a))
} else {
urlParams.set('author', searchParams.author)
}
}
if (searchParams.pubkey) { if (searchParams.pubkey) {
if (Array.isArray(searchParams.pubkey)) { if (Array.isArray(searchParams.pubkey)) {
searchParams.pubkey.forEach(p => urlParams.append('pubkey', p)) searchParams.pubkey.forEach(p => urlParams.append('pubkey', p))
@ -98,13 +93,6 @@ const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number
urlParams.set('events', searchParams.events) urlParams.set('events', searchParams.events)
} }
} }
if (searchParams.type) {
if (Array.isArray(searchParams.type)) {
searchParams.type.forEach(t => urlParams.append('type', t))
} else {
urlParams.set('type', searchParams.type)
}
}
if (searchParams.from) urlParams.set('from', searchParams.from) if (searchParams.from) urlParams.set('from', searchParams.from)
if (searchParams.to) urlParams.set('to', searchParams.to) if (searchParams.to) urlParams.set('to', searchParams.to)
if (searchParams.before) urlParams.set('before', searchParams.before) if (searchParams.before) urlParams.set('before', searchParams.before)

Loading…
Cancel
Save