Browse Source

cleaned up search

imwald
Silberengel 4 months ago
parent
commit
f4bdb8620d
  1. 50
      src/components/SearchInfo.tsx
  2. 155
      src/lib/search-parser.ts
  3. 18
      src/pages/primary/SearchPage/index.tsx
  4. 156
      src/pages/secondary/NoteListPage/index.tsx
  5. 42
      src/pages/secondary/SearchPage/index.tsx

50
src/components/SearchInfo.tsx

@ -1,4 +1,4 @@ @@ -1,4 +1,4 @@
import { Info } from 'lucide-react'
import { Info, BookOpen } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import {
@ -19,46 +19,47 @@ export default function SearchInfo() { @@ -19,46 +19,47 @@ export default function SearchInfo() {
const searchInfoContent = (
<div className="space-y-3">
<div>
<h4 className="font-semibold mb-2">Advanced Search Parameters</h4>
<h4 className="font-semibold mb-2">Search Parameters</h4>
<div className="space-y-2 text-sm">
<div>
<strong>Plain text:</strong> Searches by d-tag for replaceable events (normalized, hyphenated)
</div>
<div>
<strong>Date ranges:</strong>
<ul className="ml-4 mt-1 space-y-1 list-disc">
<li><code className="text-xs">YYYY-MM-DD to YYYY-MM-DD</code> - Date range (e.g., <code className="text-xs">2025-10-23 to 2025-10-30</code>)</li>
<li><code className="text-xs">from:YYYY-MM-DD</code> - Events from this date</li>
<li><code className="text-xs">to:YYYY-MM-DD</code> - Events until this date</li>
<li><code className="text-xs">before:YYYY-MM-DD</code> - Events before this date</li>
<li><code className="text-xs">after:YYYY-MM-DD</code> - Events after this date</li>
<li>Supports 2-digit years (e.g., <code className="text-xs">25-10-23</code> = <code className="text-xs">2025-10-23</code>)</li>
</ul>
<strong>Event IDs:</strong> Bare event IDs work as standard search (hex, note1, nevent1, naddr1)
</div>
<div>
<strong>Filters:</strong>
<ul className="ml-4 mt-1 space-y-1 list-disc">
<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">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">t:bitcoin,nostr</code>, <code className="text-xs">pubkey:npub1...,npub2...</code> or <code className="text-xs">kind:30023,2018</code></li>
<li>Multiple values supported: <code className="text-xs">t:bitcoin,nostr</code></li>
</ul>
</div>
<div>
<strong>Kind filter:</strong> Use URL parameter <code className="text-xs">k=</code> with other filters (e.g., <code className="text-xs">?t=bitcoin&k=1</code> or <code className="text-xs">?t=testfile&k=30023</code>). Cannot be used alone.
</div>
<div className="pt-2 border-t">
<p className="text-xs text-muted-foreground">
<strong>Examples:</strong>
</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>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>
<li><code>t:bitcoin</code> hashtag search</li>
<li><code>note1abc...</code> searches for event ID</li>
</ul>
</div>
</div>
</div>
<div className="pt-2 border-t">
<a
href="https://next-alexandria.gitcitadel.eu/events"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<BookOpen className="h-4 w-4" />
<span>Advanced search on Alexandria</span>
</a>
</div>
</div>
)
@ -85,6 +86,17 @@ export default function SearchInfo() { @@ -85,6 +86,17 @@ export default function SearchInfo() {
<div className="px-4 pb-4 max-h-[60vh] overflow-y-auto">
{searchInfoContent}
</div>
<div className="px-4 pb-4 border-t">
<a
href="https://next-alexandria.gitcitadel.eu/events"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 text-sm text-muted-foreground hover:text-foreground transition-colors"
>
<BookOpen className="h-4 w-4" />
<span>Advanced search on Alexandria</span>
</a>
</div>
<DrawerClose asChild>
<Button variant="outline" className="m-4">Close</Button>
</DrawerClose>

155
src/lib/search-parser.ts

@ -1,16 +1,14 @@ @@ -1,16 +1,14 @@
/**
* 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
* - 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)
* - Event IDs: Bare event IDs (hex, note1, nevent1, naddr1) work as standard search
* - 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.
* Note:
* - Nostr only supports single-letter tag indexes (#d, #t, #p, #e, #a, etc.)
* - Kind filter is only available as URL parameter k= (e.g., ?t=bitcoin&k=1)
* - Date searches and pubkey filters are not supported
*/
export interface AdvancedSearchParams {
@ -21,13 +19,10 @@ export interface AdvancedSearchParams { @@ -21,13 +19,10 @@ export interface AdvancedSearchParams {
description?: string | string[]
author?: string | string[]
pubkey?: string | string[] // Accepts: hex, npub, nprofile, or NIP-05
events?: string | string[] // Accepts: hex event ID, note, nevent, naddr
events?: string | string[] // Accepts: hex event ID, note, nevent, naddr (bare IDs work as standard search)
type?: string | string[]
from?: string // YYYY-MM-DD
to?: string // YYYY-MM-DD
before?: string // YYYY-MM-DD
after?: string // YYYY-MM-DD
kinds?: number[]
// Date searches removed - not supported
// Kind filter only available as URL parameter k=
}
/**
@ -43,28 +38,6 @@ export function normalizeToDTag(term: string): string { @@ -43,28 +38,6 @@ export function normalizeToDTag(term: string): string {
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
}
/**
* Normalize date to YYYY-MM-DD format
* Supports both 2-digit (YY) and 4-digit (YYYY) years
*/
function normalizeDate(dateStr: string): string {
const parts = dateStr.split('-')
if (parts.length !== 3) return dateStr
let year = parts[0]
const month = parts[1]
const day = parts[2]
// Convert 2-digit year to 4-digit
if (year.length === 2) {
const yearNum = parseInt(year)
// Assume years 00-30 are 2000-2030, years 31-99 are 1931-1999
year = yearNum <= 30 ? `20${year.padStart(2, '0')}` : `19${year}`
}
return `${year}-${month}-${day}`
}
/**
* Parse advanced search query
*/
@ -75,16 +48,14 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams { @@ -75,16 +48,14 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams {
.replace(/\s+/g, ' ') // Replace multiple whitespace with single space
.replace(/\s*,\s*/g, ',') // Normalize spaces around commas
.replace(/\s*:\s*/g, ':') // Normalize spaces around colons
.replace(/\s+to\s+/gi, ' to ') // Normalize "to" in date ranges
const params: AdvancedSearchParams = {}
// Regular expressions for different parameter types
// 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|hashtag|t):"([^"]+)"/gi
const unquotedPattern = /(title|subject|description|author|pubkey|type|kind|events|hashtag|t):([^\s]+)/gi
// Note: Date searches, kind: prefix, and pubkey: prefix removed
// Kind only available as URL parameter k=
const quotedPattern = /(title|subject|description|author|type|hashtag|t):"([^"]+)"/gi
const unquotedPattern = /(title|subject|description|author|type|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)
@ -188,14 +159,6 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams { @@ -188,14 +159,6 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams {
case 'type':
params.type = values.length === 1 ? values[0] : values
break
case 'pubkey':
const pubkeyValues = parseValues(value)
params.pubkey = pubkeyValues.length === 1 ? pubkeyValues[0] : pubkeyValues
break
case 'events':
const eventValues = parseValues(value)
params.events = eventValues.length === 1 ? eventValues[0] : eventValues
break
}
}
@ -246,38 +209,17 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams { @@ -246,38 +209,17 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams {
params.author = values.length === 1 ? values[0] : values
}
break
case 'pubkey':
if (!params.pubkey) {
const pubkeyValues = parseValues(value)
params.pubkey = pubkeyValues.length === 1 ? pubkeyValues[0] : pubkeyValues
}
break
case 'events':
if (!params.events) {
const eventValues = parseValues(value)
params.events = eventValues.length === 1 ? eventValues[0] : eventValues
}
break
case 'type':
if (!params.type) {
const values = parseValues(value)
params.type = values.length === 1 ? values[0] : values
}
break
case 'kind':
const kindValues = parseValues(value)
params.kinds = params.kinds || []
for (const kindVal of kindValues) {
const kindNum = parseInt(kindVal)
if (!isNaN(kindNum)) {
params.kinds.push(kindNum)
}
}
break
}
}
// Process detected bare event IDs (those not used as parameters)
// Note: Bare event IDs are left as plain text for standard search, not stored as filter params
for (const detectedId of detectedEventIds) {
const start = detectedId.start
// Skip if already used by a parameter pattern
@ -285,20 +227,12 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams { @@ -285,20 +227,12 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams {
continue
}
// Mark as used
// Mark as used - but don't store in params.events, leave as plain text for standard search
usedIndices.push(start, detectedId.end)
// Store the event ID in params.events
if (!params.events) {
params.events = detectedId.id
} else if (Array.isArray(params.events)) {
params.events.push(detectedId.id)
} else {
params.events = [params.events, detectedId.id]
}
}
// Process detected bare pubkey IDs (those not used as parameters)
// Note: Pubkey filters removed - not supported
for (const detectedId of detectedPubkeyIds) {
const start = detectedId.start
// Skip if already used by a parameter pattern
@ -306,66 +240,11 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams { @@ -306,66 +240,11 @@ export function parseAdvancedSearch(query: string): AdvancedSearchParams {
continue
}
// Mark as used
// Mark as used - but don't store in params.pubkey, leave as plain text
usedIndices.push(start, detectedId.end)
// Store the pubkey ID in params.pubkey
if (!params.pubkey) {
params.pubkey = detectedId.id
} else if (Array.isArray(params.pubkey)) {
params.pubkey.push(detectedId.id)
} else {
params.pubkey = [params.pubkey, detectedId.id]
}
}
// Process date range patterns first (DATE to DATE)
dateRangePattern.lastIndex = 0
while ((match = dateRangePattern.exec(normalizedQuery)) !== null) {
const startDate = normalizeDate(match[1])
const endDate = normalizeDate(match[2])
const start = match.index
const end = start + match[0].length
usedIndices.push(start, end)
lastIndex = Math.max(lastIndex, end)
// Use from/to for date ranges
params.from = startDate
params.to = endDate
}
// Process date parameters (from:, to:, before:, after:)
datePattern.lastIndex = 0
while ((match = datePattern.exec(normalizedQuery)) !== null) {
const start = match.index
// Skip if already used by date range pattern
if (usedIndices.some((idx, i) => i % 2 === 0 && start >= idx && start <= usedIndices[i + 1])) {
continue
}
const param = match[0].split(':')[0].toLowerCase()
const value = normalizeDate(match[1])
const end = start + match[0].length
usedIndices.push(start, end)
lastIndex = Math.max(lastIndex, end)
switch (param) {
case 'from':
params.from = value
break
case 'to':
params.to = value
break
case 'before':
params.before = value
break
case 'after':
params.after = value
break
}
}
// Date searches removed - not supported
// Extract plain text (everything not matched by patterns)
usedIndices.sort((a, b) => a - b)

18
src/pages/primary/SearchPage/index.tsx

@ -3,7 +3,8 @@ import SearchResult from '@/components/SearchResult' @@ -3,7 +3,8 @@ import SearchResult from '@/components/SearchResult'
import PrimaryPageLayout, { TPrimaryPageLayoutRef } from '@/layouts/PrimaryPageLayout'
import { usePrimaryPage } from '@/PageManager'
import { TSearchParams } from '@/types'
import SearchInfo from '@/components/SearchInfo'
import { BookOpen } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
const SearchPage = forwardRef((_, ref) => {
@ -50,7 +51,20 @@ const SearchPage = forwardRef((_, ref) => { @@ -50,7 +51,20 @@ const SearchPage = forwardRef((_, ref) => {
<SearchBar ref={searchBarRef} onSearch={onSearch} input={input} setInput={setInput} />
</div>
<div className="flex-shrink-0 relative z-50">
<SearchInfo />
<Button
variant="ghost"
className="h-9 shrink-0 text-muted-foreground hover:text-foreground border border-border/50 hover:border-border rounded-md px-3 gap-2"
asChild
>
<a
href="https://next-alexandria.gitcitadel.eu/events"
target="_blank"
rel="noopener noreferrer"
>
<BookOpen className="h-4 w-4" />
<span className="text-sm">Search on Alexandria</span>
</a>
</Button>
</div>
</div>
<div className="h-4"></div>

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

@ -184,159 +184,9 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid @@ -184,159 +184,9 @@ const NoteListPage = forwardRef<HTMLDivElement, NoteListPageProps>(({ index, hid
return
}
// Advanced search parameters (support multiple values)
// 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 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 (searchPubkey.length > 0 || searchEvents.length > 0 || from || to || before || after) {
const filter: any = {}
// Pubkey filter (support multiple pubkeys: hex, npub, nprofile, NIP-05)
if (searchPubkey.length > 0) {
const decodedPubkeys: string[] = []
for (const pubkeyInput of searchPubkey) {
try {
// Check if it's a NIP-05 identifier
if (pubkeyInput.includes('@')) {
// Will need to resolve NIP-05, but for now we'll handle it separately
// For now, try to fetch and decode
const pubkeys = await fetchPubkeysFromDomain(pubkeyInput.split('@')[1])
decodedPubkeys.push(...pubkeys)
} else if (pubkeyInput.startsWith('npub') || pubkeyInput.startsWith('nprofile')) {
const decoded = nip19.decode(pubkeyInput)
if (decoded.type === 'npub') {
decodedPubkeys.push(decoded.data)
} else if (decoded.type === 'nprofile') {
decodedPubkeys.push(decoded.data.pubkey)
}
} else {
// Assume hex pubkey
decodedPubkeys.push(pubkeyInput)
}
} catch (e) {
// If decoding fails, try as hex or skip
if (/^[a-f0-9]{64}$/i.test(pubkeyInput)) {
decodedPubkeys.push(pubkeyInput)
}
}
}
if (decodedPubkeys.length > 0) {
filter.authors = decodedPubkeys
}
}
// Events filter (support multiple events: hex, note, nevent, naddr)
if (searchEvents.length > 0) {
const eventIds: string[] = []
for (const eventInput of searchEvents) {
try {
if (/^[a-f0-9]{64}$/i.test(eventInput)) {
// Hex event ID
eventIds.push(eventInput)
} else if (eventInput.startsWith('note1') || eventInput.startsWith('nevent1') || eventInput.startsWith('naddr1')) {
const decoded = nip19.decode(eventInput)
if (decoded.type === 'note') {
eventIds.push(decoded.data)
} else if (decoded.type === 'nevent') {
eventIds.push(decoded.data.id)
} else if (decoded.type === 'naddr') {
// For naddr, we need to filter by kind, pubkey, and d-tag
if (!filter.kinds) filter.kinds = []
if (!filter.kinds.includes(decoded.data.kind)) {
filter.kinds.push(decoded.data.kind)
}
if (!filter.authors) filter.authors = []
if (!filter.authors.includes(decoded.data.pubkey)) {
filter.authors.push(decoded.data.pubkey)
}
if (decoded.data.identifier) {
if (!filter['#d']) filter['#d'] = []
if (!filter['#d'].includes(decoded.data.identifier)) {
filter['#d'].push(decoded.data.identifier)
}
}
continue // Skip adding to eventIds for naddr
}
}
} catch (e) {
// Skip invalid event IDs
}
}
if (eventIds.length > 0) {
filter.ids = eventIds
}
}
// Date filters - convert to unix timestamps
let since: number | undefined
let until: number | undefined
if (from) {
const date = new Date(from + 'T00:00:00Z')
since = Math.floor(date.getTime() / 1000)
}
if (to) {
const date = new Date(to + 'T23:59:59Z')
until = Math.floor(date.getTime() / 1000)
}
if (before) {
const date = new Date(before + 'T00:00:00Z')
until = Math.min(until || Infinity, Math.floor(date.getTime() / 1000) - 1)
}
if (after) {
const date = new Date(after + 'T23:59:59Z')
since = Math.max(since || 0, Math.floor(date.getTime() / 1000) + 1)
}
if (since) filter.since = since
if (until) filter.until = until
// Kinds filter
if (kinds.length > 0) {
filter.kinds = kinds
}
// Build title from search params
const titleParts: string[] = []
// 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 (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}`)
if (to) dateParts.push(`to:${to}`)
if (before) dateParts.push(`before:${before}`)
if (after) dateParts.push(`after:${after}`)
titleParts.push(dateParts.join(', '))
}
setTitle(`Search: ${titleParts.join(' ')}`)
setData({
type: 'search',
kinds: kinds.length > 0 ? kinds : undefined
})
setSubRequests([
{
filter,
urls: BIG_RELAY_URLS
}
])
return
}
// Advanced search parameters removed
// Note: Only hashtag (t=) and kind (k=) URL parameters are supported
// Date searches, pubkey filters, and event filters removed - not supported
}, [pubkey, relayList, handleSubscribeHashtag, push, t, isSubscribed, subscribe, client])
// Initialize on mount

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

@ -5,7 +5,8 @@ import { toSearch } from '@/lib/link' @@ -5,7 +5,8 @@ import { toSearch } from '@/lib/link'
import { parseAdvancedSearch } from '@/lib/search-parser'
import { useSecondaryPage } from '@/PageManager'
import { TSearchParams } from '@/types'
import SearchInfo from '@/components/SearchInfo'
import { BookOpen } from 'lucide-react'
import { Button } from '@/components/ui/button'
import { forwardRef, useEffect, useMemo, useRef, useState } from 'react'
const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number; hideTitlebar?: boolean }, ref) => {
@ -79,27 +80,9 @@ const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number @@ -79,27 +80,9 @@ const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number
}
// 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))
} else {
urlParams.set('pubkey', searchParams.pubkey)
}
}
if (searchParams.events) {
if (Array.isArray(searchParams.events)) {
searchParams.events.forEach(e => urlParams.append('events', e))
} else {
urlParams.set('events', searchParams.events)
}
}
if (searchParams.from) urlParams.set('from', searchParams.from)
if (searchParams.to) urlParams.set('to', searchParams.to)
if (searchParams.before) urlParams.set('before', searchParams.before)
if (searchParams.after) urlParams.set('after', searchParams.after)
if (searchParams.kinds) {
searchParams.kinds.forEach(k => urlParams.append('k', k.toString()))
}
// Note: Bare event IDs are handled as standard search, not as filter params
// Date searches and pubkey filters removed - not supported
// Kind filter only available as URL parameter k=, not from search parser
push(`/notes?${urlParams.toString()}`)
return
@ -126,7 +109,20 @@ const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number @@ -126,7 +109,20 @@ const SearchPage = forwardRef(({ index, hideTitlebar = false }: { index?: number
<SearchBar ref={searchBarRef} input={input} setInput={setInput} onSearch={onSearch} />
</div>
<div className="flex-shrink-0 relative z-50">
<SearchInfo />
<Button
variant="ghost"
className="h-9 shrink-0 text-muted-foreground hover:text-foreground border border-border/50 hover:border-border rounded-md px-3 gap-2"
asChild
>
<a
href="https://next-alexandria.gitcitadel.eu/events"
target="_blank"
rel="noopener noreferrer"
>
<BookOpen className="h-4 w-4" />
<span className="text-sm">Search on Alexandria</span>
</a>
</Button>
</div>
</div>
<div className="h-4"></div>

Loading…
Cancel
Save