From ac48cd6c15e3664c47734e44f4eb1e04ca4e87eb Mon Sep 17 00:00:00 2001 From: silberengel Date: Sun, 13 Jul 2025 09:22:17 +0200 Subject: [PATCH] second-order search results and appended t: search results added to d: search --- src/lib/components/EventSearch.svelte | 187 ++++++++++++++++++++--- src/routes/events/+page.svelte | 207 +++++++++++++++++++++++++- 2 files changed, 374 insertions(+), 20 deletions(-) diff --git a/src/lib/components/EventSearch.svelte b/src/lib/components/EventSearch.svelte index 68b5358..3ad6e96 100644 --- a/src/lib/components/EventSearch.svelte +++ b/src/lib/components/EventSearch.svelte @@ -2,7 +2,7 @@ import { Input, Button } from "flowbite-svelte"; import { Spinner } from "flowbite-svelte"; import { ndkInstance } from "$lib/ndk"; - import { fetchEventWithFallback } from "$lib/utils/nostrUtils"; + import { fetchEventWithFallback, getMatchingTags } from "$lib/utils/nostrUtils"; import { nip19 } from "$lib/utils/nostrUtils"; import { goto } from "$app/navigation"; import type { NDKEvent } from "$lib/utils/nostrUtils"; @@ -25,7 +25,7 @@ searchValue: string | null; dTagValue: string | null; onEventFound: (event: NDKEvent) => void; - onSearchResults: (results: NDKEvent[]) => void; + onSearchResults: (firstOrder: NDKEvent[], secondOrder: NDKEvent[], tTagEvents: NDKEvent[], eventIds: Set, addresses: Set) => void; event: NDKEvent | null; onClear?: () => void; onLoadingChange?: (loading: boolean) => void; @@ -86,30 +86,179 @@ if (eventArray.length === 0) { localError = `No events found with d-tag: ${normalizedDTag}`; - onSearchResults([]); + onSearchResults([], [], [], new Set(), new Set()); searching = false; if (onLoadingChange) { onLoadingChange(false); } return; - } else if (eventArray.length === 1) { - // If only one event found, treat it as a single event result - handleFoundEvent(eventArray[0]); - searching = false; - if (onLoadingChange) { onLoadingChange(false); } - return; - } else { - // Multiple events found, show as search results - console.log( - `[Events] Found ${eventArray.length} events with d-tag: ${normalizedDTag}`, + } + + // Collect all event IDs and addresses for second-order search + const eventIds = new Set(); + const eventAddresses = new Set(); + + eventArray.forEach(event => { + if (event.id) { + eventIds.add(event.id); + } + // Add a-tag addresses (kind:pubkey:d) + const aTags = getMatchingTags(event, "a"); + aTags.forEach((tag: string[]) => { + if (tag[1]) { + eventAddresses.add(tag[1]); + } + }); + }); + + // Search for second-order events that reference the original events + const secondOrderEvents = new Set(); + + if (eventIds.size > 0 || eventAddresses.size > 0) { + console.log("[Events] Searching for second-order events..."); + + // Search for events with e tags referencing the original events + if (eventIds.size > 0) { + const eTagFilter = { "#e": Array.from(eventIds) }; + const eTagEvents = await ndk.fetchEvents( + eTagFilter, + { closeOnEose: true }, + relaySet, + ); + eTagEvents.forEach(event => secondOrderEvents.add(event)); + } + + // Search for events with a tags referencing the original events + if (eventAddresses.size > 0) { + const aTagFilter = { "#a": Array.from(eventAddresses) }; + const aTagEvents = await ndk.fetchEvents( + aTagFilter, + { closeOnEose: true }, + relaySet, + ); + aTagEvents.forEach(event => secondOrderEvents.add(event)); + } + + // Search for events with content containing nevent/naddr/note references + // This is a more complex search that requires fetching recent events and checking content + // Limit the search to recent events to avoid performance issues + const recentEvents = await ndk.fetchEvents( + { + limit: 500, // Reduced limit for better performance + since: Math.floor(Date.now() / 1000) - (7 * 24 * 60 * 60) // Last 7 days + }, + { closeOnEose: true }, + relaySet, ); - onSearchResults(eventArray); - searching = false; - if (onLoadingChange) { onLoadingChange(false); } - return; + + recentEvents.forEach(event => { + if (event.content) { + // Check for nevent references with more precise matching + eventIds.forEach(id => { + // Look for complete nevent references + const neventPattern = new RegExp(`nevent1[a-z0-9]{50,}`, 'i'); + const matches = event.content.match(neventPattern); + if (matches) { + // Verify the nevent contains the event ID + matches.forEach(match => { + try { + const decoded = nip19.decode(match); + if (decoded && decoded.type === 'nevent' && decoded.data.id === id) { + secondOrderEvents.add(event); + } + } catch (e) { + // Invalid nevent, skip + } + }); + } + }); + + // Check for naddr references with more precise matching + eventAddresses.forEach(address => { + const naddrPattern = new RegExp(`naddr1[a-z0-9]{50,}`, 'i'); + const matches = event.content.match(naddrPattern); + if (matches) { + // Verify the naddr contains the address + matches.forEach(match => { + try { + const decoded = nip19.decode(match); + if (decoded && decoded.type === 'naddr') { + const decodedAddress = `${decoded.data.kind}:${decoded.data.pubkey}:${decoded.data.identifier}`; + if (decodedAddress === address) { + secondOrderEvents.add(event); + } + } + } catch (e) { + // Invalid naddr, skip + } + }); + } + }); + + // Check for note references (event IDs) with more precise matching + eventIds.forEach(id => { + const notePattern = new RegExp(`note1[a-z0-9]{50,}`, 'i'); + const matches = event.content.match(notePattern); + if (matches) { + // Verify the note contains the event ID + matches.forEach(match => { + try { + const decoded = nip19.decode(match); + if (decoded && decoded.type === 'note' && decoded.data === id) { + secondOrderEvents.add(event); + } + } catch (e) { + // Invalid note, skip + } + }); + } + }); + } + }); } + + // Combine first-order and second-order events + const allEvents = [...eventArray, ...Array.from(secondOrderEvents)]; + + // Remove duplicates based on event ID + const uniqueEvents = new Map(); + allEvents.forEach(event => { + if (event.id) { + uniqueEvents.set(event.id, event); + } + }); + + const finalEvents = Array.from(uniqueEvents.values()); + + // Separate first-order and second-order events + const firstOrderSet = new Set(eventArray.map(e => e.id)); + const firstOrder = finalEvents.filter(e => firstOrderSet.has(e.id)); + const secondOrder = finalEvents.filter(e => !firstOrderSet.has(e.id)); + + // Remove kind 7 (emoji reactions) from both first-order and second-order results + const filteredFirstOrder = firstOrder.filter(e => e.kind !== 7); + const filteredSecondOrder = secondOrder.filter(e => e.kind !== 7); + + // --- t: search --- + // Search for events with a matching t-tag (topic/tag) + const tTagFilter = { '#t': [normalizedDTag] }; + const tTagEventsSet = await ndk.fetchEvents( + tTagFilter, + { closeOnEose: true }, + relaySet, + ); + // Remove any events already in first or second order + const tTagEvents = Array.from(tTagEventsSet).filter(e => + e.kind !== 7 && + !firstOrderSet.has(e.id) && + !filteredSecondOrder.some(se => se.id === e.id) + ); + + onSearchResults(filteredFirstOrder, filteredSecondOrder, tTagEvents, eventIds, eventAddresses); + searching = false; + if (onLoadingChange) { onLoadingChange(false); } + return; } catch (err) { console.error("[Events] Error searching by d-tag:", err); - localError = "Error searching for events with this d-tag."; - onSearchResults([]); + onSearchResults([], [], [], new Set(), new Set()); searching = false; if (onLoadingChange) { onLoadingChange(false); } return; diff --git a/src/routes/events/+page.svelte b/src/routes/events/+page.svelte index 8c87db1..2a1c12a 100644 --- a/src/routes/events/+page.svelte +++ b/src/routes/events/+page.svelte @@ -25,6 +25,10 @@ let dTagValue = $state(null); let event = $state(null); let searchResults = $state([]); + let secondOrderResults = $state([]); + let tTagResults = $state([]); + let originalEventIds = $state>(new Set()); + let originalAddresses = $state>(new Set()); let profile = $state<{ name?: string; display_name?: string; @@ -40,6 +44,10 @@ function handleEventFound(newEvent: NDKEvent) { event = newEvent; searchResults = []; + secondOrderResults = []; + tTagResults = []; + originalEventIds = new Set(); + originalAddresses = new Set(); if (newEvent.kind === 0) { try { profile = JSON.parse(newEvent.content); @@ -51,8 +59,12 @@ } } - function handleSearchResults(results: NDKEvent[]) { + function handleSearchResults(results: NDKEvent[], secondOrder: NDKEvent[] = [], tTagEvents: NDKEvent[] = [], eventIds: Set = new Set(), addresses: Set = new Set()) { searchResults = results; + secondOrderResults = secondOrder; + tTagResults = tTagEvents; + originalEventIds = eventIds; + originalAddresses = addresses; event = null; profile = null; } @@ -70,6 +82,44 @@ return getMatchingTags(event, "deferral")[0]?.[1]; } + function getReferenceType(event: NDKEvent, originalEventIds: Set, originalAddresses: Set): string { + // Check if this event has e-tags referencing original events + const eTags = getMatchingTags(event, "e"); + for (const tag of eTags) { + if (originalEventIds.has(tag[1])) { + return "Reply/Reference (e-tag)"; + } + } + + // Check if this event has a-tags referencing original events + const aTags = getMatchingTags(event, "a"); + for (const tag of aTags) { + if (originalAddresses.has(tag[1])) { + return "Reply/Reference (a-tag)"; + } + } + + // Check if this event has content references + if (event.content) { + for (const id of originalEventIds) { + const neventPattern = new RegExp(`nevent1[a-z0-9]{50,}`, 'i'); + const notePattern = new RegExp(`note1[a-z0-9]{50,}`, 'i'); + if (neventPattern.test(event.content) || notePattern.test(event.content)) { + return "Content Reference"; + } + } + + for (const address of originalAddresses) { + const naddrPattern = new RegExp(`naddr1[a-z0-9]{50,}`, 'i'); + if (naddrPattern.test(event.content)) { + return "Content Reference"; + } + } + } + + return "Reference"; + } + function getNeventAddress(event: NDKEvent): string { return neventEncode(event, standardRelays); } @@ -260,5 +310,160 @@ {/if} + + {#if secondOrderResults.length > 0} +
+ + Second-Order Events (References, Replies, Quotes) ({secondOrderResults.length} + events) + +

+ Events that reference, reply to, highlight, or quote the original events. +

+
+ {#each secondOrderResults as result, index} + + {/each} +
+
+ {/if} + + {#if tTagResults.length > 0} +
+ + Search Results for t-tag: "{dTagValue?.toLowerCase()}" ({tTagResults.length} + events) + +

+ Events that are tagged with the t-tag. +

+
+ {#each tTagResults as result, index} + + {/each} +
+
+ {/if}