Browse Source

second-order search results and appended t: search results added to d: search

master
silberengel 8 months ago
parent
commit
ac48cd6c15
  1. 183
      src/lib/components/EventSearch.svelte
  2. 207
      src/routes/events/+page.svelte

183
src/lib/components/EventSearch.svelte

@ -2,7 +2,7 @@ @@ -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 @@ @@ -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<string>, addresses: Set<string>) => void;
event: NDKEvent | null;
onClear?: () => void;
onLoadingChange?: (loading: boolean) => void;
@ -86,30 +86,179 @@ @@ -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<string>();
const eventAddresses = new Set<string>();
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<NDKEvent>();
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,
);
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<string, NDKEvent>();
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(eventArray);
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;

207
src/routes/events/+page.svelte

@ -25,6 +25,10 @@ @@ -25,6 +25,10 @@
let dTagValue = $state<string | null>(null);
let event = $state<NDKEvent | null>(null);
let searchResults = $state<NDKEvent[]>([]);
let secondOrderResults = $state<NDKEvent[]>([]);
let tTagResults = $state<NDKEvent[]>([]);
let originalEventIds = $state<Set<string>>(new Set());
let originalAddresses = $state<Set<string>>(new Set());
let profile = $state<{
name?: string;
display_name?: string;
@ -40,6 +44,10 @@ @@ -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 @@ @@ -51,8 +59,12 @@
}
}
function handleSearchResults(results: NDKEvent[]) {
function handleSearchResults(results: NDKEvent[], secondOrder: NDKEvent[] = [], tTagEvents: NDKEvent[] = [], eventIds: Set<string> = new Set(), addresses: Set<string> = new Set()) {
searchResults = results;
secondOrderResults = secondOrder;
tTagResults = tTagEvents;
originalEventIds = eventIds;
originalAddresses = addresses;
event = null;
profile = null;
}
@ -70,6 +82,44 @@ @@ -70,6 +82,44 @@
return getMatchingTags(event, "deferral")[0]?.[1];
}
function getReferenceType(event: NDKEvent, originalEventIds: Set<string>, originalAddresses: Set<string>): 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 @@ @@ -260,5 +310,160 @@
</div>
</div>
{/if}
{#if secondOrderResults.length > 0}
<div class="mt-8">
<Heading tag="h2" class="h-leather mb-4">
Second-Order Events (References, Replies, Quotes) ({secondOrderResults.length}
events)
</Heading>
<P class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Events that reference, reply to, highlight, or quote the original events.
</P>
<div class="space-y-4">
{#each secondOrderResults as result, index}
<button
class="w-full text-left border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-primary-800/50 hover:bg-gray-100 dark:hover:bg-primary-700 focus:bg-gray-100 dark:focus:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors overflow-hidden"
onclick={() => handleEventFound(result)}
>
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-gray-800 dark:text-gray-100"
>Reference {index + 1}</span
>
<span class="text-xs text-gray-600 dark:text-gray-400"
>Kind: {result.kind}</span
>
<span class="text-xs text-gray-600 dark:text-gray-400">
{@render userBadge(
toNpub(result.pubkey) as string,
undefined,
)}
</span>
<span
class="text-xs text-gray-500 dark:text-gray-400 ml-auto"
>
{result.created_at
? new Date(result.created_at * 1000).toLocaleDateString()
: "Unknown date"}
</span>
</div>
<div class="text-xs text-blue-600 dark:text-blue-400 mb-1">
{getReferenceType(result, originalEventIds, originalAddresses)}
</div>
{#if getSummary(result)}
<div
class="text-sm text-primary-900 dark:text-primary-200 mb-1 line-clamp-2"
>
{getSummary(result)}
</div>
{/if}
{#if getDeferralNaddr(result)}
<div
class="text-xs text-primary-800 dark:text-primary-300 mb-1"
>
Read
<a
class="underline text-primary-700 dark:text-primary-400 hover:text-primary-900 dark:hover:text-primary-200 break-all"
href={"/publications?d=" +
encodeURIComponent((dTagValue || "").toLowerCase())}
onclick={(e) => e.stopPropagation()}
tabindex="0"
>
{getDeferralNaddr(result)}
</a>
</div>
{/if}
{#if result.content}
<div
class="text-sm text-gray-800 dark:text-gray-200 mt-1 line-clamp-2 break-words"
>
{result.content.slice(0, 200)}{result.content.length > 200
? "..."
: ""}
</div>
{/if}
</div>
</button>
{/each}
</div>
</div>
{/if}
{#if tTagResults.length > 0}
<div class="mt-8">
<Heading tag="h2" class="h-leather mb-4">
Search Results for t-tag: "{dTagValue?.toLowerCase()}" ({tTagResults.length}
events)
</Heading>
<P class="mb-4 text-sm text-gray-600 dark:text-gray-400">
Events that are tagged with the t-tag.
</P>
<div class="space-y-4">
{#each tTagResults as result, index}
<button
class="w-full text-left border border-gray-300 dark:border-gray-600 rounded-lg p-4 bg-gray-50 dark:bg-primary-800/50 hover:bg-gray-100 dark:hover:bg-primary-700 focus:bg-gray-100 dark:focus:bg-primary-700 focus:outline-none focus:ring-2 focus:ring-primary-500 transition-colors overflow-hidden"
onclick={() => handleEventFound(result)}
>
<div class="flex flex-col gap-1">
<div class="flex items-center gap-2 mb-1">
<span class="font-medium text-gray-800 dark:text-gray-100"
>Tagged Event {index + 1}</span
>
<span class="text-xs text-gray-600 dark:text-gray-400"
>Kind: {result.kind}</span
>
<span class="text-xs text-gray-600 dark:text-gray-400">
{@render userBadge(
toNpub(result.pubkey) as string,
undefined,
)}
</span>
<span
class="text-xs text-gray-500 dark:text-gray-400 ml-auto"
>
{result.created_at
? new Date(result.created_at * 1000).toLocaleDateString()
: "Unknown date"}
</span>
</div>
{#if getSummary(result)}
<div
class="text-sm text-primary-900 dark:text-primary-200 mb-1 line-clamp-2"
>
{getSummary(result)}
</div>
{/if}
{#if getDeferralNaddr(result)}
<div
class="text-xs text-primary-800 dark:text-primary-300 mb-1"
>
Read
<a
class="underline text-primary-700 dark:text-primary-400 hover:text-primary-900 dark:hover:text-primary-200 break-all"
href={"/publications?d=" +
encodeURIComponent((dTagValue || "").toLowerCase())}
onclick={(e) => e.stopPropagation()}
tabindex="0"
>
{getDeferralNaddr(result)}
</a>
</div>
{/if}
{#if result.content}
<div
class="text-sm text-gray-800 dark:text-gray-200 mt-1 line-clamp-2 break-words"
>
{result.content.slice(0, 200)}{result.content.length > 200
? "..."
: ""}
</div>
{/if}
</div>
</button>
{/each}
</div>
</div>
{/if}
</main>
</div>

Loading…
Cancel
Save