From f4bdb8620d2bab5978d7a45929187a4fbb48765a Mon Sep 17 00:00:00 2001 From: Silberengel Date: Tue, 11 Nov 2025 15:02:02 +0100 Subject: [PATCH] cleaned up search --- src/components/SearchInfo.tsx | 50 ++++--- src/lib/search-parser.ts | 155 +++----------------- src/pages/primary/SearchPage/index.tsx | 18 ++- src/pages/secondary/NoteListPage/index.tsx | 156 +-------------------- src/pages/secondary/SearchPage/index.tsx | 42 +++--- 5 files changed, 86 insertions(+), 335 deletions(-) diff --git a/src/components/SearchInfo.tsx b/src/components/SearchInfo.tsx index df27a44..0b95ceb 100644 --- a/src/components/SearchInfo.tsx +++ b/src/components/SearchInfo.tsx @@ -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() { const searchInfoContent = (
-

Advanced Search Parameters

+

Search Parameters

Plain text: Searches by d-tag for replaceable events (normalized, hyphenated)
- Date ranges: -
    -
  • YYYY-MM-DD to YYYY-MM-DD - Date range (e.g., 2025-10-23 to 2025-10-30)
  • -
  • from:YYYY-MM-DD - Events from this date
  • -
  • to:YYYY-MM-DD - Events until this date
  • -
  • before:YYYY-MM-DD - Events before this date
  • -
  • after:YYYY-MM-DD - Events after this date
  • -
  • Supports 2-digit years (e.g., 25-10-23 = 2025-10-23)
  • -
+ Event IDs: Bare event IDs work as standard search (hex, note1, nevent1, naddr1)
Filters:
  • t:hashtag or hashtag:hashtag - Filter by hashtag (t-tag)
  • -
  • pubkey:npub..., pubkey:hex, pubkey:nprofile..., or pubkey:user@domain.com - Filter by pubkey (accepts npub, nprofile, hex, or NIP-05)
  • -
  • events:hex, events:note1..., events:nevent1..., or events:naddr1... - Filter by specific events (accepts hex, note, nevent, or naddr)
  • -
  • kind:30023 - Filter by event kind (e.g., 1=notes, 30023=articles, 30817/30818=wiki)
  • -
  • Multiple values supported: t:bitcoin,nostr, pubkey:npub1...,npub2... or kind:30023,2018
  • +
  • Multiple values supported: t:bitcoin,nostr
+
+ Kind filter: Use URL parameter k= with other filters (e.g., ?t=bitcoin&k=1 or ?t=testfile&k=30023). Cannot be used alone. +

Examples:

  • jumble search → searches d-tag
  • -
  • t:bitcoin from:2024-01-01
  • -
  • pubkey:npub1abc... from:2024-01-01
  • -
  • kind:30023 from:2024-01-01
  • -
  • 2025-10-23 to 2025-10-30 → date range
  • +
  • t:bitcoin → hashtag search
  • +
  • note1abc... → searches for event ID
+
+ + + Advanced search on Alexandria + +
) @@ -85,6 +86,17 @@ export default function SearchInfo() {
{searchInfoContent}
+
+ + + Advanced search on Alexandria + +
diff --git a/src/lib/search-parser.ts b/src/lib/search-parser.ts index 8724fe2..b2603c8 100644 --- a/src/lib/search-parser.ts +++ b/src/lib/search-parser.ts @@ -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 { 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 { .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 { .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 { 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 { 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 { 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 { 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) diff --git a/src/pages/primary/SearchPage/index.tsx b/src/pages/primary/SearchPage/index.tsx index f8c353a..e538617 100644 --- a/src/pages/primary/SearchPage/index.tsx +++ b/src/pages/primary/SearchPage/index.tsx @@ -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) => {
- +
diff --git a/src/pages/secondary/NoteListPage/index.tsx b/src/pages/secondary/NoteListPage/index.tsx index 09953c9..45c7972 100644 --- a/src/pages/secondary/NoteListPage/index.tsx +++ b/src/pages/secondary/NoteListPage/index.tsx @@ -184,159 +184,9 @@ const NoteListPage = forwardRef(({ 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 diff --git a/src/pages/secondary/SearchPage/index.tsx b/src/pages/secondary/SearchPage/index.tsx index 2248988..7774fae 100644 --- a/src/pages/secondary/SearchPage/index.tsx +++ b/src/pages/secondary/SearchPage/index.tsx @@ -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 } // 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
- +