Browse Source

updated search, to be more precise

imwald
Silberengel 4 months ago
parent
commit
48ae98f003
  1. 15
      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

15
src/components/SearchBar/index.tsx

@ -343,7 +343,10 @@ function NormalItem({ @@ -343,7 +343,10 @@ function NormalItem({
}) {
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">FULL TEXT</span>
</div>
<div className="font-semibold truncate">{search}</div>
</Item>
)
@ -360,7 +363,10 @@ function HashtagItem({ @@ -360,7 +363,10 @@ function HashtagItem({
}) {
return (
<Item onClick={onClick} selected={selected}>
<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>
</Item>
)
@ -377,7 +383,10 @@ function NoteItem({ @@ -377,7 +383,10 @@ function NoteItem({
}) {
return (
<Item onClick={onClick} selected={selected}>
<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>
</Item>
)
@ -413,7 +422,10 @@ function DTagItem({ @@ -413,7 +422,10 @@ function DTagItem({
}) {
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">D-TAG</span>
</div>
<div className="font-semibold truncate">{dtag}</div>
</Item>
)
@ -430,7 +442,10 @@ function RelayItem({ @@ -430,7 +442,10 @@ function RelayItem({
}) {
return (
<Item onClick={onClick} selected={selected}>
<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>
</Item>
)

15
src/components/SearchInfo.tsx

@ -36,17 +36,13 @@ export default function SearchInfo() { @@ -36,17 +36,13 @@ export default function SearchInfo() {
</ul>
</div>
<div>
<strong>Metadata fields:</strong>
<strong>Filters:</strong>
<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">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">t:hashtag</code> or <code className="text-xs">hashtag:hashtag</code> - Filter by hashtag (t-tag)</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">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>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>
</div>
<div className="pt-2 border-t">
@ -55,8 +51,9 @@ export default function SearchInfo() { @@ -55,8 +51,9 @@ export default function SearchInfo() {
</p>
<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>title:"My Article" from:2024-01-01</code></li>
<li><code>author:"John Doe" type:wiki</code></li>
<li><code>t:bitcoin from:2024-01-01</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>
</ul>
</div>

31
src/lib/search-parser.ts

@ -2,18 +2,20 @@ @@ -2,18 +2,20 @@
* Advanced search parser for Nostr events
* 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
* - Title: title:"text" or title:text
* - Subject: subject:"text" or subject:text
* - Description: description:"text" or description:text
* - Author: author:"name" (author tag, not pubkey)
* - Pubkey: pubkey:npub... or pubkey:hex...
* - Type: type:value
* - Hashtag: t:hashtag or hashtag:hashtag (filters by #t tag)
* - Pubkey: pubkey:npub... or pubkey:hex... (filters by authors field)
* - Events: events:hex, events:note1..., events:nevent1..., events:naddr1... (filters by ids field)
* - 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 {
dtag?: string
hashtag?: string | string[] // t-tag/hashtag (uses #t tag)
title?: string | string[]
subject?: string | string[]
description?: string | string[]
@ -81,8 +83,8 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams { @@ -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)
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 quotedPattern = /(title|subject|description|author|type|pubkey|events):"([^"]+)"/gi
const unquotedPattern = /(title|subject|description|author|pubkey|type|kind|events):([^\s]+)/gi
const quotedPattern = /(title|subject|description|author|type|pubkey|events|hashtag|t):"([^"]+)"/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
// 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 { @@ -167,6 +169,10 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams {
const values = parseValues(value)
switch (param) {
case 'hashtag':
case 't':
params.hashtag = values.length === 1 ? values[0] : values
break
case 'title':
params.title = values.length === 1 ? values[0] : values
break
@ -209,6 +215,13 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams { @@ -209,6 +215,13 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams {
lastIndex = Math.max(lastIndex, end)
switch (param) {
case 'hashtag':
case 't':
if (!params.hashtag) {
const values = parseValues(value)
params.hashtag = values.length === 1 ? values[0] : values
}
break
case 'title':
if (!params.title) {
const values = parseValues(value)

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

@ -185,29 +185,19 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -185,29 +185,19 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
}
// Advanced search parameters (support multiple values)
const title = searchParams.getAll('title')
const subject = searchParams.getAll('subject')
const description = searchParams.getAll('description')
const author = searchParams.getAll('author')
// Note: Nostr only supports single-letter tag indexes, so we can't filter by
// multi-letter tags like title, subject, description, author, type
const searchPubkey = searchParams.getAll('pubkey')
const searchEvents = searchParams.getAll('events')
const type = searchParams.getAll('type')
const from = searchParams.get('from')
const to = searchParams.get('to')
const before = searchParams.get('before')
const after = searchParams.get('after')
// 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 = {}
// 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)
if (searchPubkey.length > 0) {
const decodedPubkeys: string[] = []
@ -315,16 +305,16 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -315,16 +305,16 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
// Build title from search params
const titleParts: string[] = []
if (title.length > 0) titleParts.push(`title:${title.join(',')}`)
if (subject.length > 0) titleParts.push(`subject:${subject.join(',')}`)
if (author.length > 0) titleParts.push(`author:${author.join(',')}`)
// Note: hashtag is handled separately via 't' parameter earlier in the function
if (searchPubkey.length > 0) {
const pubkeyDisplay = searchPubkey.length === 1
? `${searchPubkey[0].substring(0, 16)}...`
: `${searchPubkey.length} pubkeys`
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) {
const dateParts: string[] = []
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 @@ -46,44 +46,39 @@ const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number
const searchParams = parseAdvancedSearch(params.search)
// 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 =>
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) {
// 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()
if (searchParams.dtag) {
urlParams.set('d', searchParams.dtag)
}
if (searchParams.title) {
if (Array.isArray(searchParams.title)) {
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)
}
}
// Skip title, subject, description, author, type - these use multi-letter tags
// that Nostr relays don't index
if (searchParams.pubkey) {
if (Array.isArray(searchParams.pubkey)) {
searchParams.pubkey.forEach(p => urlParams.append('pubkey', p))
@ -98,13 +93,6 @@ const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number @@ -98,13 +93,6 @@ const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number
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.to) urlParams.set('to', searchParams.to)
if (searchParams.before) urlParams.set('before', searchParams.before)

Loading…
Cancel
Save