|
|
|
@ -220,8 +220,14 @@ |
|
|
|
/** |
|
|
|
/** |
|
|
|
* Fetches events from the Nostr network |
|
|
|
* Fetches events from the Nostr network |
|
|
|
* |
|
|
|
* |
|
|
|
* This function fetches index events and their referenced content events, |
|
|
|
* This function orchestrates the fetching of events through multiple steps: |
|
|
|
* filters them according to NIP-62, and combines them for visualization. |
|
|
|
* 1. Setup configuration and loading state |
|
|
|
|
|
|
|
* 2. Fetch non-publication events (kinds 1, 3, etc.) |
|
|
|
|
|
|
|
* 3. Fetch publication index events |
|
|
|
|
|
|
|
* 4. Extract and fetch content events |
|
|
|
|
|
|
|
* 5. Deduplicate and combine all events |
|
|
|
|
|
|
|
* 6. Fetch profiles for discovered pubkeys |
|
|
|
|
|
|
|
* 7. Apply display limits and finalize |
|
|
|
*/ |
|
|
|
*/ |
|
|
|
async function fetchEvents() { |
|
|
|
async function fetchEvents() { |
|
|
|
// Prevent concurrent fetches |
|
|
|
// Prevent concurrent fetches |
|
|
|
@ -238,339 +244,440 @@ |
|
|
|
loading = true; |
|
|
|
loading = true; |
|
|
|
error = null; |
|
|
|
error = null; |
|
|
|
|
|
|
|
|
|
|
|
// Get ALL event configurations (Phase 5: fetch all, display enabled) |
|
|
|
// Step 1: Setup configuration and loading state |
|
|
|
const config = get(visualizationConfig); |
|
|
|
const { allConfigs, publicationConfigs, otherConfigs, kind0Config } = setupFetchConfiguration(); |
|
|
|
const allConfigs = config.eventConfigs; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
debug("All event configs:", allConfigs); |
|
|
|
// Step 2: Fetch non-publication events |
|
|
|
debug("Disabled kinds:", config.disabledKinds); |
|
|
|
const nonPublicationEvents = await fetchNonPublicationEvents(otherConfigs); |
|
|
|
|
|
|
|
|
|
|
|
// Set loading event kinds for display (show all being loaded) |
|
|
|
// Step 3: Fetch publication index events |
|
|
|
loadingEventKinds = allConfigs.map(ec => ({ |
|
|
|
const validIndexEvents = await fetchPublicationIndexEvents(publicationConfigs); |
|
|
|
kind: ec.kind, |
|
|
|
|
|
|
|
limit: ec.limit |
|
|
|
|
|
|
|
})); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Separate publication kinds from other kinds |
|
|
|
// Step 4: Extract and fetch content events |
|
|
|
const publicationKinds = [30040, 30041, 30818]; |
|
|
|
const contentEvents = await fetchContentEvents(validIndexEvents, publicationConfigs); |
|
|
|
const publicationConfigs = allConfigs.filter(ec => publicationKinds.includes(ec.kind)); |
|
|
|
|
|
|
|
const otherConfigs = allConfigs.filter(ec => !publicationKinds.includes(ec.kind)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
let allFetchedEvents: NDKEvent[] = []; |
|
|
|
// Step 5: Deduplicate and combine all events |
|
|
|
|
|
|
|
const combinedEvents = deduplicateAndCombineEvents(nonPublicationEvents, validIndexEvents, contentEvents); |
|
|
|
|
|
|
|
|
|
|
|
// First, fetch non-publication events (like kind 1, 3, etc. but NOT kind 0) |
|
|
|
// Step 6: Fetch profiles for discovered pubkeys |
|
|
|
// We'll fetch kind 0 profiles after we know which pubkeys we need |
|
|
|
const eventsWithProfiles = await fetchProfilesForEvents(combinedEvents, kind0Config); |
|
|
|
const kind0Config = otherConfigs.find(c => c.kind === 0); |
|
|
|
|
|
|
|
const nonProfileConfigs = otherConfigs.filter(c => c.kind !== 0); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (nonProfileConfigs.length > 0) { |
|
|
|
// Step 7: Apply display limits and finalize |
|
|
|
debug("Fetching non-publication events (excluding profiles):", nonProfileConfigs); |
|
|
|
finalizeEventFetch(eventsWithProfiles); |
|
|
|
|
|
|
|
|
|
|
|
for (const config of nonProfileConfigs) { |
|
|
|
} catch (e) { |
|
|
|
try { |
|
|
|
console.error("Error fetching events:", e); |
|
|
|
// Special handling for kind 3 (follow lists) |
|
|
|
error = e instanceof Error ? e.message : String(e); |
|
|
|
if (config.kind === 3) { |
|
|
|
} finally { |
|
|
|
const followEvents = await fetchFollowLists(config); |
|
|
|
loading = false; |
|
|
|
allFetchedEvents.push(...followEvents); |
|
|
|
isFetching = false; |
|
|
|
} else { |
|
|
|
debug("Loading set to false in fetchEvents"); |
|
|
|
const fetchedEvents = await $ndkInstance.fetchEvents( |
|
|
|
debug("Final state check - loading:", loading, "events.length:", events.length, "allEvents.length:", allEvents.length); |
|
|
|
{ |
|
|
|
} |
|
|
|
kinds: [config.kind], |
|
|
|
} |
|
|
|
limit: config.limit |
|
|
|
|
|
|
|
}, |
|
|
|
/** |
|
|
|
{ |
|
|
|
* Step 1: Setup configuration and loading state |
|
|
|
groupable: true, |
|
|
|
*/ |
|
|
|
skipVerification: false, |
|
|
|
function setupFetchConfiguration() { |
|
|
|
skipValidation: false, |
|
|
|
const config = get(visualizationConfig); |
|
|
|
} |
|
|
|
const allConfigs = config.eventConfigs; |
|
|
|
); |
|
|
|
|
|
|
|
debug(`Fetched ${fetchedEvents.size} events of kind ${config.kind}`); |
|
|
|
debug("All event configs:", allConfigs); |
|
|
|
allFetchedEvents.push(...Array.from(fetchedEvents)); |
|
|
|
debug("Enabled kinds:", allConfigs.filter(ec => ec.enabled !== false).map(ec => ec.kind)); |
|
|
|
} |
|
|
|
|
|
|
|
} catch (e) { |
|
|
|
// Set loading event kinds for display (show all being loaded) |
|
|
|
console.error(`Error fetching kind ${config.kind}:`, e); |
|
|
|
loadingEventKinds = allConfigs.map(ec => ({ |
|
|
|
|
|
|
|
kind: ec.kind, |
|
|
|
|
|
|
|
limit: ec.limit |
|
|
|
|
|
|
|
})); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Separate publication kinds from other kinds |
|
|
|
|
|
|
|
const publicationKinds = [30040, 30041, 30818]; |
|
|
|
|
|
|
|
const publicationConfigs = allConfigs.filter(ec => publicationKinds.includes(ec.kind)); |
|
|
|
|
|
|
|
const otherConfigs = allConfigs.filter(ec => !publicationKinds.includes(ec.kind)); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Find kind 0 config for profile fetching |
|
|
|
|
|
|
|
const kind0Config = otherConfigs.find(c => c.kind === 0); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return { allConfigs, publicationConfigs, otherConfigs, kind0Config }; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Step 2: Fetch non-publication events (kinds 1, 3, etc. but NOT kind 0) |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
async function fetchNonPublicationEvents(otherConfigs: any[]): Promise<NDKEvent[]> { |
|
|
|
|
|
|
|
const nonProfileConfigs = otherConfigs.filter(c => c.kind !== 0); |
|
|
|
|
|
|
|
let allFetchedEvents: NDKEvent[] = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (nonProfileConfigs.length > 0) { |
|
|
|
|
|
|
|
debug("Fetching non-publication events (excluding profiles):", nonProfileConfigs); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for (const config of nonProfileConfigs) { |
|
|
|
|
|
|
|
try { |
|
|
|
|
|
|
|
// Special handling for kind 3 (follow lists) |
|
|
|
|
|
|
|
if (config.kind === 3) { |
|
|
|
|
|
|
|
const followEvents = await fetchFollowLists(config); |
|
|
|
|
|
|
|
allFetchedEvents.push(...followEvents); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
const fetchedEvents = await $ndkInstance.fetchEvents( |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
kinds: [config.kind], |
|
|
|
|
|
|
|
limit: config.limit |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
groupable: true, |
|
|
|
|
|
|
|
skipVerification: false, |
|
|
|
|
|
|
|
skipValidation: false, |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
debug(`Fetched ${fetchedEvents.size} events of kind ${config.kind}`); |
|
|
|
|
|
|
|
allFetchedEvents.push(...Array.from(fetchedEvents)); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} catch (e) { |
|
|
|
|
|
|
|
console.error(`Error fetching kind ${config.kind}:`, e); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
// Then handle publication events as before |
|
|
|
|
|
|
|
let validIndexEvents: Set<NDKEvent> = new Set(); |
|
|
|
return allFetchedEvents; |
|
|
|
const shouldFetchIndex = publicationConfigs.some(ec => ec.kind === INDEX_EVENT_KIND); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (data.eventId) { |
|
|
|
/** |
|
|
|
// Fetch specific publication |
|
|
|
* Step 3: Fetch publication index events |
|
|
|
debug(`Fetching specific publication: ${data.eventId}`); |
|
|
|
*/ |
|
|
|
const event = await $ndkInstance.fetchEvent(data.eventId); |
|
|
|
async function fetchPublicationIndexEvents(publicationConfigs: any[]): Promise<Set<NDKEvent>> { |
|
|
|
|
|
|
|
const shouldFetchIndex = publicationConfigs.some(ec => ec.kind === INDEX_EVENT_KIND); |
|
|
|
if (!event) { |
|
|
|
|
|
|
|
throw new Error(`Publication not found: ${data.eventId}`); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (event.kind !== INDEX_EVENT_KIND) { |
|
|
|
|
|
|
|
throw new Error(`Event ${data.eventId} is not a publication index (kind ${INDEX_EVENT_KIND})`); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
validIndexEvents = new Set([event]); |
|
|
|
|
|
|
|
} else if (!shouldFetchIndex) { |
|
|
|
|
|
|
|
debug("Index events (30040) are disabled, skipping fetch"); |
|
|
|
|
|
|
|
validIndexEvents = new Set(); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Original behavior: fetch all publications |
|
|
|
|
|
|
|
debug(`Fetching index events (kind ${INDEX_EVENT_KIND})`); |
|
|
|
|
|
|
|
const indexConfig = publicationConfigs.find(ec => ec.kind === INDEX_EVENT_KIND); |
|
|
|
|
|
|
|
const indexLimit = indexConfig?.limit || 20; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const indexEvents = await $ndkInstance.fetchEvents( |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
kinds: [INDEX_EVENT_KIND], |
|
|
|
|
|
|
|
limit: indexLimit |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
groupable: true, |
|
|
|
|
|
|
|
skipVerification: false, |
|
|
|
|
|
|
|
skipValidation: false, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
debug("Fetched index events:", indexEvents.size); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Filter valid index events according to NIP-62 |
|
|
|
if (data.eventId) { |
|
|
|
validIndexEvents = filterValidIndexEvents(indexEvents); |
|
|
|
// Fetch specific publication |
|
|
|
debug("Valid index events after filtering:", validIndexEvents.size); |
|
|
|
debug(`Fetching specific publication: ${data.eventId}`); |
|
|
|
|
|
|
|
const event = await $ndkInstance.fetchEvent(data.eventId); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (!event) { |
|
|
|
|
|
|
|
throw new Error(`Publication not found: ${data.eventId}`); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (event.kind !== INDEX_EVENT_KIND) { |
|
|
|
|
|
|
|
throw new Error(`Event ${data.eventId} is not a publication index (kind ${INDEX_EVENT_KIND})`); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return new Set([event]); |
|
|
|
|
|
|
|
} else if (!shouldFetchIndex) { |
|
|
|
|
|
|
|
debug("Index events (30040) are disabled, skipping fetch"); |
|
|
|
|
|
|
|
return new Set(); |
|
|
|
|
|
|
|
} else { |
|
|
|
|
|
|
|
// Original behavior: fetch all publications |
|
|
|
|
|
|
|
debug(`Fetching index events (kind ${INDEX_EVENT_KIND})`); |
|
|
|
|
|
|
|
const indexConfig = publicationConfigs.find(ec => ec.kind === INDEX_EVENT_KIND); |
|
|
|
|
|
|
|
const indexLimit = indexConfig?.limit || 20; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const indexEvents = await $ndkInstance.fetchEvents( |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
kinds: [INDEX_EVENT_KIND], |
|
|
|
|
|
|
|
limit: indexLimit |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
{ |
|
|
|
|
|
|
|
groupable: true, |
|
|
|
|
|
|
|
skipVerification: false, |
|
|
|
|
|
|
|
skipValidation: false, |
|
|
|
|
|
|
|
}, |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
debug("Fetched index events:", indexEvents.size); |
|
|
|
|
|
|
|
|
|
|
|
// Step 3: Extract content event references from index events |
|
|
|
// Filter valid index events according to NIP-62 |
|
|
|
const contentReferences = new Map<string, { kind: number; pubkey: string; dTag: string }>(); |
|
|
|
const validIndexEvents = filterValidIndexEvents(indexEvents); |
|
|
|
validIndexEvents.forEach((event) => { |
|
|
|
debug("Valid index events after filtering:", validIndexEvents.size); |
|
|
|
const aTags = event.getMatchingTags("a"); |
|
|
|
|
|
|
|
debug(`Event ${event.id} has ${aTags.length} a-tags`); |
|
|
|
return validIndexEvents; |
|
|
|
|
|
|
|
} |
|
|
|
aTags.forEach((tag) => { |
|
|
|
} |
|
|
|
// Parse the 'a' tag identifier: kind:pubkey:d-tag |
|
|
|
|
|
|
|
if (tag[1]) { |
|
|
|
/** |
|
|
|
const parts = tag[1].split(':'); |
|
|
|
* Step 4: Extract and fetch content events |
|
|
|
if (parts.length >= 3) { |
|
|
|
*/ |
|
|
|
const kind = parseInt(parts[0]); |
|
|
|
async function fetchContentEvents(validIndexEvents: Set<NDKEvent>, publicationConfigs: any[]): Promise<Set<NDKEvent>> { |
|
|
|
const pubkey = parts[1]; |
|
|
|
// Extract content event references from index events |
|
|
|
const dTag = parts.slice(2).join(':'); // Handle d-tags with colons |
|
|
|
const contentReferences = extractContentReferences(validIndexEvents); |
|
|
|
|
|
|
|
debug("Content references to fetch:", contentReferences.size); |
|
|
|
// Only add if it's a content event kind we're interested in |
|
|
|
|
|
|
|
if (CONTENT_EVENT_KINDS.includes(kind)) { |
|
|
|
// Fetch the referenced content events with author filter |
|
|
|
const key = `${kind}:${pubkey}:${dTag}`; |
|
|
|
const enabledPublicationKinds = publicationConfigs.map(ec => ec.kind); |
|
|
|
contentReferences.set(key, { kind, pubkey, dTag }); |
|
|
|
const enabledContentKinds = CONTENT_EVENT_KINDS.filter(kind => enabledPublicationKinds.includes(kind)); |
|
|
|
} |
|
|
|
debug(`Fetching content events (enabled kinds: ${enabledContentKinds.join(', ')})`); |
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
// Group by author to make more efficient queries |
|
|
|
|
|
|
|
const referencesByAuthor = groupContentReferencesByAuthor(contentReferences, enabledContentKinds); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch events for each author |
|
|
|
|
|
|
|
const contentEventPromises = Array.from(referencesByAuthor.entries()).map( |
|
|
|
|
|
|
|
async ([author, refs]) => { |
|
|
|
|
|
|
|
const dTags = [...new Set(refs.map(r => r.dTag))]; // Dedupe d-tags |
|
|
|
|
|
|
|
return $ndkInstance.fetchEvents({ |
|
|
|
|
|
|
|
kinds: enabledContentKinds, // Only fetch enabled kinds |
|
|
|
|
|
|
|
authors: [author], |
|
|
|
|
|
|
|
"#d": dTags, |
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
} |
|
|
|
debug("Content references to fetch:", contentReferences.size); |
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const contentEventSets = await Promise.all(contentEventPromises); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Deduplicate by keeping only the most recent version of each d-tag per author |
|
|
|
|
|
|
|
const eventsByCoordinate = deduplicateContentEvents(contentEventSets); |
|
|
|
|
|
|
|
const contentEvents = new Set(eventsByCoordinate.values()); |
|
|
|
|
|
|
|
debug("Fetched content events after deduplication:", contentEvents.size); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return contentEvents; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Step 4: Fetch the referenced content events with author filter |
|
|
|
/** |
|
|
|
// Only fetch content kinds that are enabled |
|
|
|
* Extract content event references from index events |
|
|
|
const enabledPublicationKinds = publicationConfigs.map(ec => ec.kind); |
|
|
|
*/ |
|
|
|
const enabledContentKinds = CONTENT_EVENT_KINDS.filter(kind => enabledPublicationKinds.includes(kind)); |
|
|
|
function extractContentReferences(validIndexEvents: Set<NDKEvent>): Map<string, { kind: number; pubkey: string; dTag: string }> { |
|
|
|
debug(`Fetching content events (enabled kinds: ${enabledContentKinds.join(', ')})`); |
|
|
|
const contentReferences = new Map<string, { kind: number; pubkey: string; dTag: string }>(); |
|
|
|
|
|
|
|
|
|
|
|
// Group by author to make more efficient queries |
|
|
|
validIndexEvents.forEach((event) => { |
|
|
|
const referencesByAuthor = new Map<string, Array<{ kind: number; dTag: string }>>(); |
|
|
|
const aTags = event.getMatchingTags("a"); |
|
|
|
contentReferences.forEach(({ kind, pubkey, dTag }) => { |
|
|
|
debug(`Event ${event.id} has ${aTags.length} a-tags`); |
|
|
|
// Only include references for enabled kinds |
|
|
|
|
|
|
|
if (enabledContentKinds.includes(kind)) { |
|
|
|
aTags.forEach((tag) => { |
|
|
|
if (!referencesByAuthor.has(pubkey)) { |
|
|
|
// Parse the 'a' tag identifier: kind:pubkey:d-tag |
|
|
|
referencesByAuthor.set(pubkey, []); |
|
|
|
if (tag[1]) { |
|
|
|
|
|
|
|
const parts = tag[1].split(':'); |
|
|
|
|
|
|
|
if (parts.length >= 3) { |
|
|
|
|
|
|
|
const kind = parseInt(parts[0]); |
|
|
|
|
|
|
|
const pubkey = parts[1]; |
|
|
|
|
|
|
|
const dTag = parts.slice(2).join(':'); // Handle d-tags with colons |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Only add if it's a content event kind we're interested in |
|
|
|
|
|
|
|
if (CONTENT_EVENT_KINDS.includes(kind)) { |
|
|
|
|
|
|
|
const key = `${kind}:${pubkey}:${dTag}`; |
|
|
|
|
|
|
|
contentReferences.set(key, { kind, pubkey, dTag }); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
referencesByAuthor.get(pubkey)!.push({ kind, dTag }); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
}); |
|
|
|
// Fetch events for each author |
|
|
|
|
|
|
|
const contentEventPromises = Array.from(referencesByAuthor.entries()).map( |
|
|
|
return contentReferences; |
|
|
|
async ([author, refs]) => { |
|
|
|
} |
|
|
|
const dTags = [...new Set(refs.map(r => r.dTag))]; // Dedupe d-tags |
|
|
|
|
|
|
|
return $ndkInstance.fetchEvents({ |
|
|
|
/** |
|
|
|
kinds: enabledContentKinds, // Only fetch enabled kinds |
|
|
|
* Group content references by author for efficient fetching |
|
|
|
authors: [author], |
|
|
|
*/ |
|
|
|
"#d": dTags, |
|
|
|
function groupContentReferencesByAuthor( |
|
|
|
}); |
|
|
|
contentReferences: Map<string, { kind: number; pubkey: string; dTag: string }>, |
|
|
|
|
|
|
|
enabledContentKinds: number[] |
|
|
|
|
|
|
|
): Map<string, Array<{ kind: number; dTag: string }>> { |
|
|
|
|
|
|
|
const referencesByAuthor = new Map<string, Array<{ kind: number; dTag: string }>>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
contentReferences.forEach(({ kind, pubkey, dTag }) => { |
|
|
|
|
|
|
|
// Only include references for enabled kinds |
|
|
|
|
|
|
|
if (enabledContentKinds.includes(kind)) { |
|
|
|
|
|
|
|
if (!referencesByAuthor.has(pubkey)) { |
|
|
|
|
|
|
|
referencesByAuthor.set(pubkey, []); |
|
|
|
} |
|
|
|
} |
|
|
|
); |
|
|
|
referencesByAuthor.get(pubkey)!.push({ kind, dTag }); |
|
|
|
|
|
|
|
} |
|
|
|
const contentEventSets = await Promise.all(contentEventPromises); |
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// Deduplicate by keeping only the most recent version of each d-tag per author |
|
|
|
return referencesByAuthor; |
|
|
|
const eventsByCoordinate = new Map<string, NDKEvent>(); |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
contentEventSets.forEach((eventSet, idx) => { |
|
|
|
/** |
|
|
|
eventSet.forEach(event => { |
|
|
|
* Deduplicate content events by keeping only the most recent version |
|
|
|
const dTag = event.tagValue("d"); |
|
|
|
*/ |
|
|
|
const author = event.pubkey; |
|
|
|
function deduplicateContentEvents(contentEventSets: Set<NDKEvent>[]): Map<string, NDKEvent> { |
|
|
|
const kind = event.kind; |
|
|
|
const eventsByCoordinate = new Map<string, NDKEvent>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
contentEventSets.forEach((eventSet) => { |
|
|
|
|
|
|
|
eventSet.forEach(event => { |
|
|
|
|
|
|
|
const dTag = event.tagValue("d"); |
|
|
|
|
|
|
|
const author = event.pubkey; |
|
|
|
|
|
|
|
const kind = event.kind; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (dTag && author && kind) { |
|
|
|
|
|
|
|
const coordinate = `${kind}:${author}:${dTag}`; |
|
|
|
|
|
|
|
const existing = eventsByCoordinate.get(coordinate); |
|
|
|
|
|
|
|
|
|
|
|
if (dTag && author && kind) { |
|
|
|
// Keep the most recent event (highest created_at) |
|
|
|
const coordinate = `${kind}:${author}:${dTag}`; |
|
|
|
if (!existing || (event.created_at && existing.created_at && event.created_at > existing.created_at)) { |
|
|
|
const existing = eventsByCoordinate.get(coordinate); |
|
|
|
eventsByCoordinate.set(coordinate, event); |
|
|
|
|
|
|
|
debug(`Keeping newer version of ${coordinate}, created_at: ${event.created_at}`); |
|
|
|
// Keep the most recent event (highest created_at) |
|
|
|
} else if (existing) { |
|
|
|
if (!existing || (event.created_at && existing.created_at && event.created_at > existing.created_at)) { |
|
|
|
debug(`Skipping older version of ${coordinate}, created_at: ${event.created_at} vs ${existing.created_at}`); |
|
|
|
eventsByCoordinate.set(coordinate, event); |
|
|
|
|
|
|
|
debug(`Keeping newer version of ${coordinate}, created_at: ${event.created_at}`); |
|
|
|
|
|
|
|
} else if (existing) { |
|
|
|
|
|
|
|
debug(`Skipping older version of ${coordinate}, created_at: ${event.created_at} vs ${existing.created_at}`); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
}); |
|
|
|
const contentEvents = new Set(eventsByCoordinate.values()); |
|
|
|
|
|
|
|
debug("Fetched content events after deduplication:", contentEvents.size); |
|
|
|
return eventsByCoordinate; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Step 5: Combine all events (non-publication + publication events) |
|
|
|
/** |
|
|
|
// First, build coordinate map for replaceable events |
|
|
|
* Step 5: Deduplicate and combine all events |
|
|
|
const coordinateMap = new Map<string, NDKEvent>(); |
|
|
|
*/ |
|
|
|
const allEventsToProcess = [ |
|
|
|
function deduplicateAndCombineEvents( |
|
|
|
...allFetchedEvents, // Non-publication events fetched earlier |
|
|
|
nonPublicationEvents: NDKEvent[], |
|
|
|
...Array.from(validIndexEvents), |
|
|
|
validIndexEvents: Set<NDKEvent>, |
|
|
|
...Array.from(contentEvents) |
|
|
|
contentEvents: Set<NDKEvent> |
|
|
|
]; |
|
|
|
): NDKEvent[] { |
|
|
|
|
|
|
|
// First, build coordinate map for replaceable events |
|
|
|
|
|
|
|
const coordinateMap = new Map<string, NDKEvent>(); |
|
|
|
|
|
|
|
const allEventsToProcess = [ |
|
|
|
|
|
|
|
...nonPublicationEvents, // Non-publication events fetched earlier |
|
|
|
|
|
|
|
...Array.from(validIndexEvents), |
|
|
|
|
|
|
|
...Array.from(contentEvents) |
|
|
|
|
|
|
|
]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// First pass: identify the most recent version of each replaceable event |
|
|
|
|
|
|
|
allEventsToProcess.forEach(event => { |
|
|
|
|
|
|
|
if (!event.id) return; |
|
|
|
|
|
|
|
|
|
|
|
// First pass: identify the most recent version of each replaceable event |
|
|
|
// For replaceable events (30000-39999), track by coordinate |
|
|
|
allEventsToProcess.forEach(event => { |
|
|
|
if (event.kind && event.kind >= 30000 && event.kind < 40000) { |
|
|
|
if (!event.id) return; |
|
|
|
const dTag = event.tagValue("d"); |
|
|
|
|
|
|
|
const author = event.pubkey; |
|
|
|
|
|
|
|
|
|
|
|
// For replaceable events (30000-39999), track by coordinate |
|
|
|
if (dTag && author) { |
|
|
|
if (event.kind && event.kind >= 30000 && event.kind < 40000) { |
|
|
|
const coordinate = `${event.kind}:${author}:${dTag}`; |
|
|
|
const dTag = event.tagValue("d"); |
|
|
|
const existing = coordinateMap.get(coordinate); |
|
|
|
const author = event.pubkey; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (dTag && author) { |
|
|
|
// Keep the most recent version |
|
|
|
const coordinate = `${event.kind}:${author}:${dTag}`; |
|
|
|
if (!existing || (event.created_at && existing.created_at && event.created_at > existing.created_at)) { |
|
|
|
const existing = coordinateMap.get(coordinate); |
|
|
|
coordinateMap.set(coordinate, event); |
|
|
|
|
|
|
|
|
|
|
|
// Keep the most recent version |
|
|
|
|
|
|
|
if (!existing || (event.created_at && existing.created_at && event.created_at > existing.created_at)) { |
|
|
|
|
|
|
|
coordinateMap.set(coordinate, event); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
}); |
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
// Second pass: build final event map |
|
|
|
|
|
|
|
const finalEventMap = new Map<string, NDKEvent>(); |
|
|
|
// Second pass: build final event map |
|
|
|
const seenCoordinates = new Set<string>(); |
|
|
|
const finalEventMap = new Map<string, NDKEvent>(); |
|
|
|
|
|
|
|
const seenCoordinates = new Set<string>(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
allEventsToProcess.forEach(event => { |
|
|
|
|
|
|
|
if (!event.id) return; |
|
|
|
|
|
|
|
|
|
|
|
allEventsToProcess.forEach(event => { |
|
|
|
// For replaceable events, only add if it's the chosen version |
|
|
|
if (!event.id) return; |
|
|
|
if (event.kind && event.kind >= 30000 && event.kind < 40000) { |
|
|
|
|
|
|
|
const dTag = event.tagValue("d"); |
|
|
|
|
|
|
|
const author = event.pubkey; |
|
|
|
|
|
|
|
|
|
|
|
// For replaceable events, only add if it's the chosen version |
|
|
|
if (dTag && author) { |
|
|
|
if (event.kind && event.kind >= 30000 && event.kind < 40000) { |
|
|
|
const coordinate = `${event.kind}:${author}:${dTag}`; |
|
|
|
const dTag = event.tagValue("d"); |
|
|
|
const chosenEvent = coordinateMap.get(coordinate); |
|
|
|
const author = event.pubkey; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if (dTag && author) { |
|
|
|
// Only add this event if it's the chosen one for this coordinate |
|
|
|
const coordinate = `${event.kind}:${author}:${dTag}`; |
|
|
|
if (chosenEvent && chosenEvent.id === event.id) { |
|
|
|
const chosenEvent = coordinateMap.get(coordinate); |
|
|
|
if (!seenCoordinates.has(coordinate)) { |
|
|
|
|
|
|
|
finalEventMap.set(event.id, event); |
|
|
|
// Only add this event if it's the chosen one for this coordinate |
|
|
|
seenCoordinates.add(coordinate); |
|
|
|
if (chosenEvent && chosenEvent.id === event.id) { |
|
|
|
|
|
|
|
if (!seenCoordinates.has(coordinate)) { |
|
|
|
|
|
|
|
finalEventMap.set(event.id, event); |
|
|
|
|
|
|
|
seenCoordinates.add(coordinate); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
return; |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
// Non-replaceable events are added directly |
|
|
|
|
|
|
|
finalEventMap.set(event.id, event); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Replace mode (always replace, no append mode) |
|
|
|
|
|
|
|
allEvents = Array.from(finalEventMap.values()); |
|
|
|
|
|
|
|
followListEvents = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
baseEvents = [...allEvents]; // Store base events for tag expansion |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Step 6: Extract all pubkeys and fetch profiles |
|
|
|
|
|
|
|
debug("Extracting pubkeys from all events"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Use the utility function to extract ALL pubkeys (authors + p tags + content) |
|
|
|
|
|
|
|
const allPubkeys = extractPubkeysFromEvents(allEvents); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if follow list is configured with limit > 0 |
|
|
|
|
|
|
|
const followListConfig = allConfigs.find(c => c.kind === 3); |
|
|
|
|
|
|
|
const shouldIncludeFollowPubkeys = followListConfig && followListConfig.limit > 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add pubkeys from follow lists only if follow list limit > 0 |
|
|
|
|
|
|
|
if (shouldIncludeFollowPubkeys && followListEvents.length > 0) { |
|
|
|
|
|
|
|
debug("Including pubkeys from follow lists (limit > 0)"); |
|
|
|
|
|
|
|
followListEvents.forEach(event => { |
|
|
|
|
|
|
|
if (event.pubkey) allPubkeys.add(event.pubkey); |
|
|
|
|
|
|
|
event.tags.forEach(tag => { |
|
|
|
|
|
|
|
if (tag[0] === 'p' && tag[1]) { |
|
|
|
|
|
|
|
allPubkeys.add(tag[1]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
} else if (!shouldIncludeFollowPubkeys && followListEvents.length > 0) { |
|
|
|
|
|
|
|
debug("Excluding follow list pubkeys (limit = 0, only fetching event authors)"); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
debug("Profile extraction complete:", { |
|
|
|
// Non-replaceable events are added directly |
|
|
|
totalPubkeys: allPubkeys.size, |
|
|
|
finalEventMap.set(event.id, event); |
|
|
|
fromEvents: allEvents.length, |
|
|
|
}); |
|
|
|
fromFollowLists: followListEvents.length |
|
|
|
|
|
|
|
|
|
|
|
// Replace mode (always replace, no append mode) |
|
|
|
|
|
|
|
allEvents = Array.from(finalEventMap.values()); |
|
|
|
|
|
|
|
followListEvents = []; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
baseEvents = [...allEvents]; // Store base events for tag expansion |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return allEvents; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Step 6: Fetch profiles for discovered pubkeys |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
async function fetchProfilesForEvents(combinedEvents: NDKEvent[], kind0Config: any): Promise<NDKEvent[]> { |
|
|
|
|
|
|
|
// Extract all pubkeys and fetch profiles |
|
|
|
|
|
|
|
debug("Extracting pubkeys from all events"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Use the utility function to extract ALL pubkeys (authors + p tags + content) |
|
|
|
|
|
|
|
const allPubkeys = extractPubkeysFromEvents(combinedEvents); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Check if follow list is configured with limit > 0 |
|
|
|
|
|
|
|
const allConfigs = get(visualizationConfig).eventConfigs; |
|
|
|
|
|
|
|
const followListConfig = allConfigs.find(c => c.kind === 3); |
|
|
|
|
|
|
|
const shouldIncludeFollowPubkeys = followListConfig && followListConfig.limit > 0; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add pubkeys from follow lists only if follow list limit > 0 |
|
|
|
|
|
|
|
if (shouldIncludeFollowPubkeys && followListEvents.length > 0) { |
|
|
|
|
|
|
|
debug("Including pubkeys from follow lists (limit > 0)"); |
|
|
|
|
|
|
|
followListEvents.forEach(event => { |
|
|
|
|
|
|
|
if (event.pubkey) allPubkeys.add(event.pubkey); |
|
|
|
|
|
|
|
event.tags.forEach(tag => { |
|
|
|
|
|
|
|
if (tag[0] === 'p' && tag[1]) { |
|
|
|
|
|
|
|
allPubkeys.add(tag[1]); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
}); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
} else if (!shouldIncludeFollowPubkeys && followListEvents.length > 0) { |
|
|
|
|
|
|
|
debug("Excluding follow list pubkeys (limit = 0, only fetching event authors)"); |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
debug("Profile extraction complete:", { |
|
|
|
|
|
|
|
totalPubkeys: allPubkeys.size, |
|
|
|
|
|
|
|
fromEvents: combinedEvents.length, |
|
|
|
|
|
|
|
fromFollowLists: followListEvents.length |
|
|
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Fetch ALL profiles if kind 0 is enabled |
|
|
|
|
|
|
|
let profileEvents: NDKEvent[] = []; |
|
|
|
|
|
|
|
if (kind0Config) { |
|
|
|
|
|
|
|
debug("Fetching profiles for all discovered pubkeys"); |
|
|
|
|
|
|
|
|
|
|
|
// Fetch ALL profiles if kind 0 is enabled |
|
|
|
// Update progress during fetch |
|
|
|
let profileEvents: NDKEvent[] = []; |
|
|
|
profileLoadingProgress = { current: 0, total: allPubkeys.size }; |
|
|
|
if (kind0Config) { |
|
|
|
|
|
|
|
debug("Fetching profiles for all discovered pubkeys"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Update progress during fetch |
|
|
|
|
|
|
|
profileLoadingProgress = { current: 0, total: allPubkeys.size }; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
profileEvents = await batchFetchProfiles( |
|
|
|
|
|
|
|
Array.from(allPubkeys), |
|
|
|
|
|
|
|
(fetched, total) => { |
|
|
|
|
|
|
|
profileLoadingProgress = { current: fetched, total }; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
profileLoadingProgress = null; |
|
|
|
|
|
|
|
debug("Profile fetch complete, fetched", profileEvents.length, "profiles"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Add profile events to allEvents |
|
|
|
|
|
|
|
allEvents = [...allEvents, ...profileEvents]; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Update profile stats for display |
|
|
|
|
|
|
|
// Use the total number of pubkeys, not just newly fetched profiles |
|
|
|
|
|
|
|
profileStats = { |
|
|
|
|
|
|
|
totalFetched: allPubkeys.size, |
|
|
|
|
|
|
|
displayLimit: kind0Config.limit |
|
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Step 7: Apply display limits |
|
|
|
profileEvents = await batchFetchProfiles( |
|
|
|
events = filterByDisplayLimits(allEvents, $visualizationConfig); |
|
|
|
Array.from(allPubkeys), |
|
|
|
|
|
|
|
(fetched, total) => { |
|
|
|
|
|
|
|
profileLoadingProgress = { current: fetched, total }; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
); |
|
|
|
|
|
|
|
|
|
|
|
// Step 8: Detect missing events |
|
|
|
profileLoadingProgress = null; |
|
|
|
const eventIds = new Set(allEvents.map(e => e.id)); |
|
|
|
debug("Profile fetch complete, fetched", profileEvents.length, "profiles"); |
|
|
|
missingEventIds = detectMissingEvents(events, eventIds); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
debug("Total events fetched:", allEvents.length); |
|
|
|
// Add profile events to allEvents |
|
|
|
debug("Events displayed:", events.length); |
|
|
|
allEvents = [...combinedEvents, ...profileEvents]; |
|
|
|
debug("Missing event IDs:", missingEventIds.size); |
|
|
|
|
|
|
|
debug("About to set loading to false"); |
|
|
|
// Update profile stats for display |
|
|
|
debug("Current loading state:", loading); |
|
|
|
// Use the total number of pubkeys, not just newly fetched profiles |
|
|
|
} catch (e) { |
|
|
|
profileStats = { |
|
|
|
console.error("Error fetching events:", e); |
|
|
|
totalFetched: allPubkeys.size, |
|
|
|
error = e instanceof Error ? e.message : String(e); |
|
|
|
displayLimit: kind0Config.limit |
|
|
|
} finally { |
|
|
|
}; |
|
|
|
loading = false; |
|
|
|
} else { |
|
|
|
isFetching = false; |
|
|
|
allEvents = [...combinedEvents]; |
|
|
|
debug("Loading set to false in fetchEvents"); |
|
|
|
|
|
|
|
debug("Final state check - loading:", loading, "events.length:", events.length, "allEvents.length:", allEvents.length); |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
return allEvents; |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
|
|
* Step 7: Apply display limits and finalize |
|
|
|
|
|
|
|
*/ |
|
|
|
|
|
|
|
function finalizeEventFetch(eventsWithProfiles: NDKEvent[]) { |
|
|
|
|
|
|
|
// Apply display limits |
|
|
|
|
|
|
|
events = filterByDisplayLimits(eventsWithProfiles, $visualizationConfig); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Detect missing events |
|
|
|
|
|
|
|
const eventIds = new Set(eventsWithProfiles.map(e => e.id)); |
|
|
|
|
|
|
|
missingEventIds = detectMissingEvents(events, eventIds); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
debug("Total events fetched:", eventsWithProfiles.length); |
|
|
|
|
|
|
|
debug("Events displayed:", events.length); |
|
|
|
|
|
|
|
debug("Missing event IDs:", missingEventIds.size); |
|
|
|
|
|
|
|
debug("About to set loading to false"); |
|
|
|
|
|
|
|
debug("Current loading state:", loading); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -793,7 +900,7 @@ |
|
|
|
|
|
|
|
|
|
|
|
// React to display limit and allowed kinds changes |
|
|
|
// React to display limit and allowed kinds changes |
|
|
|
$effect(() => { |
|
|
|
$effect(() => { |
|
|
|
debug("Effect triggered: allEvents.length =", allEvents.length, "allowedKinds =", $visualizationConfig.allowedKinds); |
|
|
|
debug("Effect triggered: allEvents.length =", allEvents.length, "enabledKinds =", $visualizationConfig.eventConfigs.filter(ec => ec.enabled !== false).map(ec => ec.kind)); |
|
|
|
if (allEvents.length > 0) { |
|
|
|
if (allEvents.length > 0) { |
|
|
|
const newEvents = filterByDisplayLimits(allEvents, $visualizationConfig); |
|
|
|
const newEvents = filterByDisplayLimits(allEvents, $visualizationConfig); |
|
|
|
|
|
|
|
|
|
|
|
|