Browse Source

Remove Advanced Search

bug-fixes
master
Silberengel 4 weeks ago
parent
commit
7d41b7567d
  1. 672
      src/lib/components/find/AdvancedSearch.svelte
  2. 32
      src/lib/components/find/NormalSearch.svelte
  3. 4
      src/lib/components/find/SearchAddressableEvents.svelte
  4. 22
      src/routes/find/+page.svelte
  5. 4
      static/changelog.yaml

672
src/lib/components/find/AdvancedSearch.svelte

@ -1,672 +0,0 @@ @@ -1,672 +0,0 @@
<script lang="ts">
import { nostrClient } from '../../services/nostr/nostr-client.js';
import { relayManager } from '../../services/nostr/relay-manager.js';
import { nip19 } from 'nostr-tools';
import { KIND_LOOKUP } from '../../types/kind-lookup.js';
import type { NostrEvent } from '../../types/nostr.js';
interface Props {
onSearchResults?: (results: { events: NostrEvent[]; profiles: string[]; relays?: string[]; eventRelays?: Map<string, string> }) => void;
}
let { onSearchResults }: Props = $props();
// NIP-01 standard filter controls
let filterIds = $state<string>('');
let filterAuthors = $state<string>('');
let filterKinds = $state<string>('');
let filterE = $state<string>('');
let filterP = $state<string>('');
let filterA = $state<string>('');
let filterQ = $state<string>('');
let filterT = $state<string>('');
let filterC = $state<string>('');
let filterD = $state<string>('');
let filterSince = $state<string>('');
let filterUntil = $state<string>('');
let filterLimit = $state<number>(100);
let searching = $state(false);
let searchResults = $state<{ events: NostrEvent[]; profiles: string[]; relays?: string[] }>({ events: [], profiles: [] });
const eventRelayMap = new Map<string, string>();
// Helper function to decode bech32 to hex
async function decodeBech32ToHex(bech32: string, type: 'pubkey' | 'event'): Promise<string | null> {
try {
if (!/^(npub|nprofile|note|nevent|naddr)1[a-z0-9]+$/i.test(bech32)) {
return null;
}
const decoded = nip19.decode(bech32);
if (type === 'pubkey') {
if (decoded.type === 'npub') {
return String(decoded.data).toLowerCase();
} else if (decoded.type === 'nprofile') {
if (decoded.data && typeof decoded.data === 'object' && 'pubkey' in decoded.data) {
return String(decoded.data.pubkey).toLowerCase();
}
} else if (decoded.type === 'naddr') {
if (decoded.data && typeof decoded.data === 'object' && 'pubkey' in decoded.data) {
return String(decoded.data.pubkey).toLowerCase();
}
}
} else if (type === 'event') {
if (decoded.type === 'note') {
return String(decoded.data).toLowerCase();
} else if (decoded.type === 'nevent') {
if (decoded.data && typeof decoded.data === 'object' && 'id' in decoded.data) {
return String(decoded.data.id).toLowerCase();
}
}
}
} catch (error) {
console.debug('Error decoding bech32:', error);
}
return null;
}
// Helper function to parse comma-separated values and validate hex strings
async function parseHexList(input: string, type: 'pubkey' | 'event' = 'event'): Promise<string[]> {
const results: string[] = [];
const parts = input.split(',').map(s => s.trim()).filter(s => s.length > 0);
for (const part of parts) {
// Check if it's already a hex string
if (/^[0-9a-f]{64}$/i.test(part)) {
results.push(part.toLowerCase());
} else {
// Try to decode as bech32
const decoded = await decodeBech32ToHex(part, type);
if (decoded) {
results.push(decoded);
}
}
}
return results;
}
// Helper function to parse comma-separated numbers
function parseNumberList(input: string): number[] {
return input
.split(',')
.map(s => parseInt(s.trim(), 10))
.filter(n => !isNaN(n) && n >= 0 && n <= 65535);
}
// Helper function to parse comma-separated addressable event refs
async function parseAddressableList(input: string): Promise<string[]> {
const results: string[] = [];
const parts = input.split(',').map(s => s.trim()).filter(s => s.length > 0);
for (const part of parts) {
const match = part.match(/^(\d+):([^:]+)(?::(.+))?$/);
if (match) {
const [, kind, pubkeyPart, dTag] = match;
let hexPubkey: string | null = null;
if (/^[0-9a-f]{64}$/i.test(pubkeyPart)) {
hexPubkey = pubkeyPart.toLowerCase();
} else {
hexPubkey = await decodeBech32ToHex(pubkeyPart, 'pubkey');
}
if (hexPubkey) {
if (dTag) {
results.push(`${kind}:${hexPubkey}:${dTag}`);
} else {
results.push(`${kind}:${hexPubkey}:`);
}
}
}
}
return results;
}
// Helper function to convert date-time string to unix timestamp
function dateTimeToUnixTimestamp(dateTimeStr: string): number | null {
if (!dateTimeStr.trim()) return null;
try {
const date = new Date(dateTimeStr.trim());
if (isNaN(date.getTime())) {
const timestamp = parseInt(dateTimeStr.trim(), 10);
if (!isNaN(timestamp) && timestamp > 0) {
return timestamp;
}
return null;
}
return Math.floor(date.getTime() / 1000);
} catch (error) {
const timestamp = parseInt(dateTimeStr.trim(), 10);
if (!isNaN(timestamp) && timestamp > 0) {
return timestamp;
}
return null;
}
}
// Build NIP-01 filter from form inputs
async function buildFilter(): Promise<any> {
const filter: any = {};
if (filterIds.trim()) {
const ids = await parseHexList(filterIds, 'event');
if (ids.length > 0) {
filter.ids = ids;
}
}
if (filterAuthors.trim()) {
const authors = await parseHexList(filterAuthors, 'pubkey');
if (authors.length > 0) {
filter.authors = authors;
}
}
if (filterKinds.trim()) {
const kinds = parseNumberList(filterKinds);
if (kinds.length > 0) {
filter.kinds = kinds;
}
}
if (filterE.trim()) {
const eTags = await parseHexList(filterE, 'event');
if (eTags.length > 0) {
filter['#e'] = eTags;
}
}
if (filterP.trim()) {
const pTags = await parseHexList(filterP, 'pubkey');
if (pTags.length > 0) {
filter['#p'] = pTags;
}
}
if (filterA.trim()) {
const aTags = await parseAddressableList(filterA);
if (aTags.length > 0) {
filter['#a'] = aTags;
}
}
if (filterQ.trim()) {
const qTags = await parseHexList(filterQ, 'event');
if (qTags.length > 0) {
filter['#q'] = qTags;
}
}
if (filterT.trim()) {
const tTags = filterT
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
if (tTags.length > 0) {
filter['#T'] = tTags;
}
}
if (filterC.trim()) {
const cTags = filterC
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
if (cTags.length > 0) {
filter['#C'] = cTags;
}
}
if (filterD.trim()) {
const dTags = filterD
.split(',')
.map(s => s.trim())
.filter(s => s.length > 0);
if (dTags.length > 0) {
filter['#d'] = dTags;
}
}
if (filterSince.trim()) {
const since = dateTimeToUnixTimestamp(filterSince);
if (since !== null && since > 0) {
filter.since = since;
}
}
if (filterUntil.trim()) {
const until = dateTimeToUnixTimestamp(filterUntil);
if (until !== null && until > 0) {
filter.until = until;
}
}
if (filterLimit > 0) {
filter.limit = filterLimit;
} else {
filter.limit = 100;
}
return filter;
}
async function handleSearch() {
searching = true;
const newEventRelayMap = new Map<string, string>();
try {
await nostrClient.initialize();
const relays = relayManager.getAllAvailableRelays();
const filter = await buildFilter();
// Check if filter has any conditions (not just limit)
const hasConditions = Object.keys(filter).some(key => key !== 'limit');
if (!hasConditions) {
// No filter conditions, show error or return empty
searchResults = { events: [], profiles: [], relays };
if (onSearchResults) {
onSearchResults({ events: [], profiles: [], relays, eventRelays: new Map() });
}
searching = false;
return;
}
// Fetch events using the filter
const events = await nostrClient.fetchEvents(
[filter],
relays,
{ useCache: true, cacheResults: true }
);
// Track which relay each event came from
for (const event of events) {
if (!newEventRelayMap.has(event.id)) {
newEventRelayMap.set(event.id, relays[0] || 'unknown');
}
}
// Sort by created_at (newest first)
const sortedEvents = events.sort((a, b) => b.created_at - a.created_at);
searchResults = { events: sortedEvents, profiles: [], relays };
if (onSearchResults) {
onSearchResults({ events: sortedEvents, profiles: [], relays, eventRelays: newEventRelayMap });
}
} catch (error) {
console.error('Advanced search error:', error);
searchResults = { events: [], profiles: [], relays: [] };
if (onSearchResults) {
onSearchResults({ events: [], profiles: [], relays: [], eventRelays: new Map() });
}
} finally {
searching = false;
}
}
export function clearSearch() {
filterIds = '';
filterAuthors = '';
filterKinds = '';
filterE = '';
filterP = '';
filterA = '';
filterQ = '';
filterT = '';
filterC = '';
filterD = '';
filterSince = '';
filterUntil = '';
filterLimit = 100;
searchResults = { events: [], profiles: [] };
eventRelayMap.clear();
if (onSearchResults) {
onSearchResults({ events: [], profiles: [], relays: [], eventRelays: new Map() });
}
}
export function getSearchResults() {
return searchResults;
}
</script>
<div class="advanced-search">
<h2>Advanced Search (NIP-01 Filters)</h2>
<p class="section-description">
Use NIP-01 standard filters to search for events. All filters are optional - combine them as needed.
</p>
<div class="search-container">
<div class="advanced-filters-grid">
<div class="filter-group">
<label for="filter-ids" class="filter-label">IDs (comma-separated event IDs):</label>
<input
id="filter-ids"
type="text"
bind:value={filterIds}
placeholder="64-char hex event IDs or bech32 (note, nevent)"
class="filter-input"
/>
</div>
<div class="filter-group">
<label for="filter-authors" class="filter-label">Authors (comma-separated pubkeys):</label>
<input
id="filter-authors"
type="text"
bind:value={filterAuthors}
placeholder="64-char hex pubkeys or bech32 (npub, nprofile)"
class="filter-input"
/>
</div>
<div class="filter-group">
<label for="filter-kinds" class="filter-label">Kinds (comma-separated):</label>
<input
id="filter-kinds"
type="text"
bind:value={filterKinds}
placeholder="e.g., 1, 7, 11"
class="filter-input"
/>
</div>
<div class="filter-group">
<label for="filter-e" class="filter-label">#e tag (comma-separated event IDs):</label>
<input
id="filter-e"
type="text"
bind:value={filterE}
placeholder="64-char hex event IDs or bech32"
class="filter-input"
/>
</div>
<div class="filter-group">
<label for="filter-p" class="filter-label">#p tag (comma-separated pubkeys):</label>
<input
id="filter-p"
type="text"
bind:value={filterP}
placeholder="64-char hex pubkeys or bech32"
class="filter-input"
/>
</div>
<div class="filter-group">
<label for="filter-a" class="filter-label">#a tag (comma-separated addressable refs):</label>
<input
id="filter-a"
type="text"
bind:value={filterA}
placeholder="kind:pubkey:d-tag"
class="filter-input"
/>
</div>
<div class="filter-group">
<label for="filter-q" class="filter-label">#q tag (comma-separated quoted event IDs):</label>
<input
id="filter-q"
type="text"
bind:value={filterQ}
placeholder="64-char hex event IDs or bech32"
class="filter-input"
/>
</div>
<div class="filter-group">
<label for="filter-t" class="filter-label">#T tag (comma-separated topics):</label>
<input
id="filter-t"
type="text"
bind:value={filterT}
placeholder="topic1, topic2, topic3"
class="filter-input"
/>
</div>
<div class="filter-group">
<label for="filter-c" class="filter-label">#C tag (comma-separated categories):</label>
<input
id="filter-c"
type="text"
bind:value={filterC}
placeholder="category1, category2, category3"
class="filter-input"
/>
</div>
<div class="filter-group">
<label for="filter-d" class="filter-label">#d tag (comma-separated d-tag values):</label>
<input
id="filter-d"
type="text"
bind:value={filterD}
placeholder="d-tag1, d-tag2, d-tag3"
class="filter-input"
/>
</div>
<div class="filter-group">
<label for="filter-since" class="filter-label">Since (date-time or unix timestamp):</label>
<input
id="filter-since"
type="text"
bind:value={filterSince}
placeholder="2024-01-15T14:30:00Z or unix timestamp"
class="filter-input"
/>
<small class="filter-hint">Accepts ISO 8601 format or unix timestamp</small>
</div>
<div class="filter-group">
<label for="filter-until" class="filter-label">Until (date-time or unix timestamp):</label>
<input
id="filter-until"
type="text"
bind:value={filterUntil}
placeholder="2024-01-15T14:30:00Z or unix timestamp"
class="filter-input"
/>
<small class="filter-hint">Accepts ISO 8601 format or unix timestamp</small>
</div>
<div class="filter-group">
<label for="filter-limit" class="filter-label">Limit:</label>
<input
id="filter-limit"
type="number"
bind:value={filterLimit}
min="1"
max="1000"
class="filter-input"
/>
</div>
</div>
<div class="search-button-wrapper">
<button
class="search-button"
onclick={handleSearch}
disabled={searching}
aria-label="Search"
>
{searching ? 'Searching...' : 'Search'}
</button>
</div>
</div>
</div>
<style>
.advanced-search {
/* No container styling - parent .find-section handles it */
}
.advanced-search h2 {
margin: 0 0 1rem 0;
font-size: 1.25em;
font-weight: 600;
color: var(--fog-text, #475569);
}
@media (min-width: 640px) {
.advanced-search h2 {
margin: 0 0 1.5rem 0;
}
}
:global(.dark) .advanced-search h2 {
color: var(--fog-dark-text, #cbd5e1);
}
.section-description {
margin: 0 0 1rem 0;
color: var(--fog-text-light, #52667a);
font-size: 0.875em;
}
@media (min-width: 640px) {
.section-description {
margin: 0 0 1.5rem 0;
}
}
:global(.dark) .section-description {
color: var(--fog-dark-text-light, #a8b8d0);
}
.search-container {
display: flex;
flex-direction: column;
gap: 1.5rem;
}
.advanced-filters-grid {
display: grid;
grid-template-columns: 1fr;
gap: 0.75rem;
padding: 0.75rem;
background: var(--fog-highlight, #f3f4f6);
border-radius: 0.375rem;
border: 1px solid var(--fog-border, #e5e7eb);
}
@media (min-width: 640px) {
.advanced-filters-grid {
gap: 1rem;
padding: 1rem;
}
}
:global(.dark) .advanced-filters-grid {
background: var(--fog-dark-highlight, #374151);
border-color: var(--fog-dark-border, #475569);
}
@media (min-width: 768px) {
.advanced-filters-grid {
grid-template-columns: repeat(2, 1fr);
}
}
.filter-group {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.filter-label {
font-size: 0.875em;
font-weight: 500;
color: var(--fog-text, #1f2937);
}
:global(.dark) .filter-label {
color: var(--fog-dark-text, #f9fafb);
}
.filter-input {
padding: 0.5em;
border: 1px solid var(--fog-border, #cbd5e1);
border-radius: 0.375rem;
background: var(--fog-post, #ffffff);
color: var(--fog-text, #1f2937);
font-size: 0.875em;
font-family: inherit;
width: 100%;
}
.filter-input:focus {
outline: none;
border-color: var(--fog-accent, #64748b);
box-shadow: 0 0 0 3px rgba(100, 116, 139, 0.1);
}
:global(.dark) .filter-input {
border-color: var(--fog-dark-border, #374151);
background: var(--fog-dark-post, #1f2937);
color: var(--fog-dark-text, #f9fafb);
}
:global(.dark) .filter-input:focus {
border-color: var(--fog-dark-accent, #94a3b8);
box-shadow: 0 0 0 3px rgba(148, 163, 184, 0.1);
}
.filter-hint {
font-size: 0.75em;
color: var(--fog-text-light, #52667a);
margin-top: 0.25em;
display: block;
}
:global(.dark) .filter-hint {
color: var(--fog-dark-text-light, #a8b8d0);
}
.search-button-wrapper {
display: flex;
justify-content: flex-end;
}
.search-button {
padding: 0.75em 1.5em;
background: var(--fog-accent, #64748b);
color: #ffffff;
border: none;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875em;
font-weight: 500;
font-family: inherit;
white-space: nowrap;
transition: all 0.2s;
min-width: 100px;
}
:global(.dark) .search-button {
background: var(--fog-dark-accent, #94a3b8);
color: #1f2937;
}
.search-button:hover:not(:disabled) {
opacity: 0.9;
transform: translateY(-1px);
}
.search-button:active:not(:disabled) {
transform: translateY(0);
}
.search-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>

32
src/lib/components/find/NormalSearch.svelte

@ -61,9 +61,9 @@ @@ -61,9 +61,9 @@
</script>
<div class="normal-search">
<h2>Normal Search</h2>
<h2>Search Events & Profiles</h2>
<p class="section-description">
Search for events by ID, pubkey, NIP-05, or content. Use the kind filter to narrow results.
Search for events by ID (hex, note, nevent), pubkey (hex, npub, nprofile), NIP-05 address, or content text. Filter by event kind to narrow results.
</p>
<div class="search-container">
@ -74,11 +74,11 @@ @@ -74,11 +74,11 @@
selectedKind={selectedKind}
hideDropdownResults={true}
onSearchResults={onSearchResults}
placeholder="Search events, profiles, pubkeys, or enter event ID..."
placeholder="Enter event ID (hex/note/nevent), pubkey (hex/npub/nprofile), NIP-05, or search content..."
/>
</div>
<div class="filter-and-button-wrapper">
<div class="filter-wrapper">
<div class="kind-filter-wrapper">
<label for="kind-filter" class="kind-filter-label">Filter by Kind:</label>
<select
@ -94,7 +94,10 @@ @@ -94,7 +94,10 @@
{/each}
</select>
</div>
</div>
<div class="button-wrapper">
<div style="flex: 1;"></div>
<button
class="search-button"
onclick={handleSearch}
@ -155,31 +158,36 @@ @@ -155,31 +158,36 @@
width: 100%;
}
.filter-and-button-wrapper {
.filter-wrapper {
display: flex;
flex-direction: column;
gap: 1rem;
gap: 0.5rem;
}
@media (min-width: 640px) {
.filter-and-button-wrapper {
.filter-wrapper {
flex-direction: row;
align-items: flex-end;
align-items: center;
}
}
.button-wrapper {
display: flex;
flex-direction: row;
align-items: flex-end;
gap: 1rem;
}
.kind-filter-wrapper {
display: flex;
flex-direction: column;
gap: 0.5rem;
flex: 1;
}
@media (min-width: 640px) {
.kind-filter-wrapper {
flex-direction: row;
align-items: center;
flex: 1;
}
}
@ -236,13 +244,13 @@ @@ -236,13 +244,13 @@
}
.search-button {
padding: 0.75em 1.5em;
padding: 0.75rem 1.5rem;
background: var(--fog-accent, #64748b);
color: #ffffff;
border: none;
border-radius: 0.375rem;
cursor: pointer;
font-size: 0.875em;
font-size: 0.875rem;
font-weight: 500;
font-family: inherit;
white-space: nowrap;

4
src/lib/components/find/SearchAddressableEvents.svelte

@ -502,7 +502,7 @@ @@ -502,7 +502,7 @@
<div class="addressable-search">
<h2>Search Addressable Events</h2>
<p class="section-description">
Search through parameterized events with d-tags, such as long-form articles, wiki pages, or e-books/publications
Search through parameterized replaceable events (kind 30000+) by d-tag, title, author, topic, category, or description. Perfect for finding long-form articles, wiki pages, e-books, publications, and other structured content. Supports naddr format and searches both cache and relays.
</p>
<div class="search-container">
@ -510,7 +510,7 @@ @@ -510,7 +510,7 @@
<input
type="text"
bind:value={searchQuery}
placeholder="e.g., jane eyre, jane-eyre, Charlotte Bronte..."
placeholder="Search by d-tag, title, author, topic, category, or description (e.g., jane-eyre, Charlotte Bronte, nostr)..."
class="search-input"
disabled={searching}
/>

22
src/routes/find/+page.svelte

@ -2,7 +2,6 @@ @@ -2,7 +2,6 @@
import Header from '../../lib/components/layout/Header.svelte';
import PageHeader from '../../lib/components/layout/PageHeader.svelte';
import NormalSearch from '../../lib/components/find/NormalSearch.svelte';
import AdvancedSearch from '../../lib/components/find/AdvancedSearch.svelte';
import SearchAddressableEvents from '../../lib/components/find/SearchAddressableEvents.svelte';
import FeedPost from '../../lib/modules/feed/FeedPost.svelte';
import ProfileBadge from '../../lib/components/layout/ProfileBadge.svelte';
@ -18,7 +17,6 @@ @@ -18,7 +17,6 @@
import { getPaginatedItems, getCurrentPage, ITEMS_PER_PAGE } from '../../lib/utils/pagination.js';
let normalSearchComponent: { clearSearch: () => void; getSearchResults: () => { events: NostrEvent[]; profiles: string[] } } | null = $state(null);
let advancedSearchComponent: { clearSearch: () => void; getSearchResults: () => { events: NostrEvent[]; profiles: string[] } } | null = $state(null);
let addressableSearchComponent: { setSearchQuery: (query: string) => void; clearSearch: () => void } | null = $state(null);
// Combined results from both search types
@ -66,23 +64,10 @@ @@ -66,23 +64,10 @@
}
}
function handleAdvancedSearchResults(results: { events: NostrEvent[]; profiles: string[]; relays?: string[]; eventRelays?: Map<string, string> }) {
searchResults = results;
if (results.eventRelays) {
for (const [eventId, relay] of results.eventRelays) {
eventRelayMap.set(eventId, relay);
}
}
}
function clearAllSearches() {
if (normalSearchComponent) {
normalSearchComponent.clearSearch();
}
if (advancedSearchComponent) {
advancedSearchComponent.clearSearch();
}
if (addressableSearchComponent) {
addressableSearchComponent.clearSearch();
}
@ -241,13 +226,6 @@ @@ -241,13 +226,6 @@
/>
</section>
<section class="find-section">
<AdvancedSearch
bind:this={advancedSearchComponent}
onSearchResults={handleAdvancedSearchResults}
/>
</section>
<section class="find-section">
<SearchAddressableEvents bind:this={addressableSearchComponent} />
</section>

4
static/changelog.yaml

@ -2,8 +2,10 @@ versions: @@ -2,8 +2,10 @@ versions:
'0.3.2':
- 'Expanded /repos to handle GitLab, Gitea, and OneDev repositories'
- 'Added back and refresh buttons to all pages'
- 'Handle GRASP repository management'
- 'Handle GRASP repository management with message'
- 'Remove Advanced Search from Find page'
- 'Support image, documentation, banner, and primary tags on repositories'
- 'Add media tab'
'0.3.1':
- 'Media attachments rendering in all feeds and views'
- 'NIP-92/NIP-94 image tags support'

Loading…
Cancel
Save