diff --git a/src/components/EventDetailViewer.tsx b/src/components/EventDetailViewer.tsx index 0e99771..a9092e9 100644 --- a/src/components/EventDetailViewer.tsx +++ b/src/components/EventDetailViewer.tsx @@ -26,6 +26,8 @@ import { useUserParameterizedSpells } from "@/hooks/useParameterizedSpells"; import { EventFeed } from "./nostr/EventFeed"; import { useReqTimelineEnhanced } from "@/hooks/useReqTimelineEnhanced"; import { applySpellParameters, decodeSpell } from "@/lib/spell-conversion"; +import { parseReqCommand } from "@/lib/req-parser"; +import { AGGREGATOR_RELAYS } from "@/services/loaders"; export interface EventDetailViewerProps { pointer: EventPointer | AddressPointer; @@ -51,39 +53,149 @@ function SpellTabContent({ spellId, spell, targetEventId, -}: SpellTabContentProps) { - // Decode spell and apply parameters - const { appliedFilter, relays } = useMemo(() => { - if (!targetEventId || !spell.event) { - return { appliedFilter: null, relays: [] }; + targetEvent, +}: SpellTabContentProps & { targetEvent: any }) { + // Parse spell and get filter - handle both published (with event) and local (command-only) spells + const parsed = useMemo(() => { + if (!targetEventId) { + console.log(`[EventSpell:${spell.name || spellId}] No target event ID`); + return null; } try { - const parsed = decodeSpell(spell.event); - const applied = applySpellParameters(parsed, [targetEventId]); - return { - appliedFilter: applied, - relays: parsed.relays || [], + console.log(`[EventSpell:${spell.name || spellId}] Parsing spell:`, { + hasEvent: !!spell.event, + command: spell.command, + parameterType: spell.parameterType, + }); + + // If we have a published event, decode it + if (spell.event) { + const decoded = decodeSpell(spell.event); + console.log( + `[EventSpell:${spell.name || spellId}] Decoded from event:`, + { + filter: decoded.filter, + relays: decoded.relays, + parameter: decoded.parameter, + }, + ); + return decoded; + } + + // For local spells, parse the command directly + console.log( + `[EventSpell:${spell.name || spellId}] Parsing local spell command`, + ); + const commandWithoutPrefix = spell.command + .replace(/^\s*(req|count)\s+/i, "") + .trim(); + const tokens = commandWithoutPrefix.split(/\s+/); + const commandParsed = parseReqCommand(tokens); + + // Create a ParsedSpell-like object for local spells + const localParsed = { + command: spell.command, + filter: commandParsed.filter, + relays: commandParsed.relays, + closeOnEose: commandParsed.closeOnEose, + parameter: spell.parameterType + ? { + type: spell.parameterType, + default: spell.parameterDefault, + } + : undefined, }; + + console.log(`[EventSpell:${spell.name || spellId}] Parsed local spell:`, { + filter: localParsed.filter, + relays: localParsed.relays, + parameter: localParsed.parameter, + }); + + return localParsed; } catch (error) { - console.error("Failed to apply spell parameters:", error); - return { appliedFilter: null, relays: [] }; + console.error( + `[EventSpell:${spell.name || spellId}] Failed to parse spell:`, + error, + ); + return null; } - }, [spell.event, targetEventId]); + }, [spell, targetEventId, spellId]); + + // Apply parameters to get final filter + const appliedFilter = useMemo(() => { + if (!parsed || !targetEventId) return null; + + try { + const applied = applySpellParameters(parsed, [targetEventId]); + console.log(`[EventSpell:${spell.name || spellId}] Applied parameters:`, { + input: targetEventId, + result: applied, + }); + return applied; + } catch (error) { + console.error( + `[EventSpell:${spell.name || spellId}] Failed to apply parameters:`, + error, + ); + return null; + } + }, [parsed, targetEventId, spell.name, spellId]); + + // Resolve relays - use explicit relays from spell, or use relay hints from target event + const finalRelays = useMemo(() => { + // Use explicit relays from spell if provided + if (parsed?.relays && parsed.relays.length > 0) { + console.log( + `[EventSpell:${spell.name || spellId}] Using explicit relays:`, + parsed.relays, + ); + return parsed.relays; + } + + // Use relay hints from the target event + if (targetEvent) { + const seenRelaysSet = getSeenRelays(targetEvent); + if (seenRelaysSet && seenRelaysSet.size > 0) { + const eventRelays = Array.from(seenRelaysSet); + console.log( + `[EventSpell:${spell.name || spellId}] Using target event relays:`, + eventRelays, + ); + return eventRelays; + } + } + + // Fallback to aggregator relays + console.log( + `[EventSpell:${spell.name || spellId}] Using fallback AGGREGATOR_RELAYS`, + ); + return AGGREGATOR_RELAYS; + }, [parsed?.relays, targetEvent, spell.name, spellId]); // Fetch events using the applied filter - const { events, loading, eoseReceived } = appliedFilter - ? useReqTimelineEnhanced( - `spell-${spellId}-${targetEventId}`, - appliedFilter, - relays, - { limit: appliedFilter.limit || 50, stream: true }, - ) - : { - events: [], - loading: false, - eoseReceived: false, - }; + const { events, loading, eoseReceived } = + appliedFilter && finalRelays.length > 0 + ? useReqTimelineEnhanced( + `spell-${spellId}-${targetEventId}`, + appliedFilter, + finalRelays, + { limit: appliedFilter.limit || 50, stream: true }, + ) + : { + events: [], + loading: false, + eoseReceived: false, + }; + + console.log(`[EventSpell:${spell.name || spellId}] Render state:`, { + hasFilter: !!appliedFilter, + relayCount: finalRelays.length, + eventCount: events.length, + loading, + eoseReceived, + }); return ( @@ -91,6 +203,7 @@ function SpellTabContent({

Unable to apply spell to this event

+

Check console for details

) : ( @@ -296,6 +409,7 @@ export function EventDetailViewer({ pointer }: EventDetailViewerProps) { spellId={spell.id} spell={spell} targetEventId={event.id} + targetEvent={event} /> ))} diff --git a/src/components/ProfileViewer.tsx b/src/components/ProfileViewer.tsx index bee98ed..b778abb 100644 --- a/src/components/ProfileViewer.tsx +++ b/src/components/ProfileViewer.tsx @@ -39,6 +39,9 @@ import { useUserParameterizedSpells } from "@/hooks/useParameterizedSpells"; import { EventFeed } from "./nostr/EventFeed"; import { useReqTimelineEnhanced } from "@/hooks/useReqTimelineEnhanced"; import { applySpellParameters, decodeSpell } from "@/lib/spell-conversion"; +import { parseReqCommand } from "@/lib/req-parser"; +import { useOutboxRelays } from "@/hooks/useOutboxRelays"; +import { AGGREGATOR_RELAYS } from "@/services/loaders"; export interface ProfileViewerProps { pubkey: string; @@ -65,38 +68,178 @@ function SpellTabContent({ spell, targetPubkey, }: SpellTabContentProps) { - // Decode spell and apply parameters - const { appliedFilter, relays } = useMemo(() => { - if (!targetPubkey || !spell.event) { - return { appliedFilter: null, relays: [] }; + const { state } = useGrimoire(); + + // Parse spell and get filter - handle both published (with event) and local (command-only) spells + const parsed = useMemo(() => { + if (!targetPubkey) { + console.log( + `[SpellTabContent:${spell.name || spellId}] No target pubkey`, + ); + return null; } try { - const parsed = decodeSpell(spell.event); - const applied = applySpellParameters(parsed, [targetPubkey]); - return { - appliedFilter: applied, - relays: parsed.relays || [], + console.log(`[SpellTabContent:${spell.name || spellId}] Parsing spell:`, { + hasEvent: !!spell.event, + command: spell.command, + parameterType: spell.parameterType, + }); + + // If we have a published event, decode it + if (spell.event) { + const decoded = decodeSpell(spell.event); + console.log( + `[SpellTabContent:${spell.name || spellId}] Decoded from event:`, + { + filter: decoded.filter, + relays: decoded.relays, + parameter: decoded.parameter, + }, + ); + return decoded; + } + + // For local spells, parse the command directly + console.log( + `[SpellTabContent:${spell.name || spellId}] Parsing local spell command`, + ); + const commandWithoutPrefix = spell.command + .replace(/^\s*(req|count)\s+/i, "") + .trim(); + const tokens = commandWithoutPrefix.split(/\s+/); + const commandParsed = parseReqCommand(tokens); + + // Create a ParsedSpell-like object for local spells + const localParsed = { + command: spell.command, + filter: commandParsed.filter, + relays: commandParsed.relays, + closeOnEose: commandParsed.closeOnEose, + parameter: spell.parameterType + ? { + type: spell.parameterType, + default: spell.parameterDefault, + } + : undefined, }; + + console.log( + `[SpellTabContent:${spell.name || spellId}] Parsed local spell:`, + { + filter: localParsed.filter, + relays: localParsed.relays, + parameter: localParsed.parameter, + }, + ); + + return localParsed; } catch (error) { - console.error("Failed to apply spell parameters:", error); - return { appliedFilter: null, relays: [] }; + console.error( + `[SpellTabContent:${spell.name || spellId}] Failed to parse spell:`, + error, + ); + return null; } - }, [spell.event, targetPubkey]); + }, [spell, targetPubkey, spellId]); + + // Apply parameters to get final filter + const appliedFilter = useMemo(() => { + if (!parsed || !targetPubkey) return null; + + try { + const applied = applySpellParameters(parsed, [targetPubkey]); + console.log( + `[SpellTabContent:${spell.name || spellId}] Applied parameters:`, + { + input: targetPubkey, + result: applied, + }, + ); + return applied; + } catch (error) { + console.error( + `[SpellTabContent:${spell.name || spellId}] Failed to apply parameters:`, + error, + ); + return null; + } + }, [parsed, targetPubkey, spell.name, spellId]); + + // Resolve relays - use explicit relays from spell, or use NIP-65 outbox selection + const fallbackRelays = useMemo( + () => + state.activeAccount?.relays?.filter((r) => r.read).map((r) => r.url) || + AGGREGATOR_RELAYS, + [state.activeAccount?.relays], + ); + + const outboxOptions = useMemo( + () => ({ + fallbackRelays, + timeout: 1000, + maxRelays: 42, + }), + [fallbackRelays], + ); + + // Use outbox relay selection if no explicit relays provided in spell + const { relays: selectedRelays, phase: relaySelectionPhase } = + useOutboxRelays(appliedFilter || {}, outboxOptions); + + const finalRelays = useMemo(() => { + // Use explicit relays from spell if provided + if (parsed?.relays && parsed.relays.length > 0) { + console.log( + `[SpellTabContent:${spell.name || spellId}] Using explicit relays:`, + parsed.relays, + ); + return parsed.relays; + } + + // Wait for outbox relay selection to complete + if (relaySelectionPhase !== "ready") { + console.log( + `[SpellTabContent:${spell.name || spellId}] Waiting for relay selection (phase: ${relaySelectionPhase})`, + ); + return []; + } + + console.log( + `[SpellTabContent:${spell.name || spellId}] Using outbox-selected relays:`, + selectedRelays, + ); + return selectedRelays; + }, [ + parsed?.relays, + relaySelectionPhase, + selectedRelays, + spell.name, + spellId, + ]); // Fetch events using the applied filter - const { events, loading, eoseReceived } = appliedFilter - ? useReqTimelineEnhanced( - `spell-${spellId}-${targetPubkey}`, - appliedFilter, - relays, - { limit: appliedFilter.limit || 50, stream: true }, - ) - : { - events: [], - loading: false, - eoseReceived: false, - }; + const { events, loading, eoseReceived } = + appliedFilter && finalRelays.length > 0 + ? useReqTimelineEnhanced( + `spell-${spellId}-${targetPubkey}`, + appliedFilter, + finalRelays, + { limit: appliedFilter.limit || 50, stream: true }, + ) + : { + events: [], + loading: false, + eoseReceived: false, + }; + + console.log(`[SpellTabContent:${spell.name || spellId}] Render state:`, { + hasFilter: !!appliedFilter, + relayCount: finalRelays.length, + eventCount: events.length, + loading, + eoseReceived, + }); return ( @@ -104,6 +247,13 @@ function SpellTabContent({

Unable to apply spell to this profile

+

Check console for details

+
+
+ ) : finalRelays.length === 0 ? ( +
+
+

Selecting relays...

) : ( diff --git a/src/components/RelayViewer.tsx b/src/components/RelayViewer.tsx index 64e5a9c..566b700 100644 --- a/src/components/RelayViewer.tsx +++ b/src/components/RelayViewer.tsx @@ -10,9 +10,8 @@ import { EventFeed } from "./nostr/EventFeed"; import { useReqTimelineEnhanced } from "@/hooks/useReqTimelineEnhanced"; import { applySpellParameters, decodeSpell } from "@/lib/spell-conversion"; import { parseReqCommand } from "@/lib/req-parser"; -import { useMemo, useState } from "react"; -import { KindBadge } from "./KindBadge"; -import { CreateParameterizedSpellDialog } from "./CreateParameterizedSpellDialog"; +import { useMemo } from "react"; +import { NIPBadge } from "./NIPBadge"; import { SpellHeader } from "./timeline/SpellHeader"; export interface RelayViewerProps { @@ -42,32 +41,119 @@ function SpellTabContent({ }: SpellTabContentProps) { const { addWindow } = useGrimoire(); - // Decode spell and apply parameters - const { appliedFilter, relays } = useMemo(() => { - if (!targetRelay || !spell.event) { - return { appliedFilter: null, relays: [] }; + // Parse spell and get filter - handle both published (with event) and local (command-only) spells + const parsed = useMemo(() => { + if (!targetRelay) { + console.log(`[RelaySpell:${spell.name || spellId}] No target relay`); + return null; } try { - const parsed = decodeSpell(spell.event); - const applied = applySpellParameters(parsed, [targetRelay]); - return { - appliedFilter: applied, - relays: parsed.relays || [], + console.log(`[RelaySpell:${spell.name || spellId}] Parsing spell:`, { + hasEvent: !!spell.event, + command: spell.command, + parameterType: spell.parameterType, + }); + + // If we have a published event, decode it + if (spell.event) { + const decoded = decodeSpell(spell.event); + console.log( + `[RelaySpell:${spell.name || spellId}] Decoded from event:`, + { + filter: decoded.filter, + relays: decoded.relays, + parameter: decoded.parameter, + }, + ); + return decoded; + } + + // For local spells, parse the command directly + console.log( + `[RelaySpell:${spell.name || spellId}] Parsing local spell command`, + ); + const commandWithoutPrefix = spell.command + .replace(/^\s*(req|count)\s+/i, "") + .trim(); + const tokens = commandWithoutPrefix.split(/\s+/); + const commandParsed = parseReqCommand(tokens); + + // Create a ParsedSpell-like object for local spells + const localParsed = { + command: spell.command, + filter: commandParsed.filter, + relays: commandParsed.relays, + closeOnEose: commandParsed.closeOnEose, + parameter: spell.parameterType + ? { + type: spell.parameterType, + default: spell.parameterDefault, + } + : undefined, }; + + console.log(`[RelaySpell:${spell.name || spellId}] Parsed local spell:`, { + filter: localParsed.filter, + relays: localParsed.relays, + parameter: localParsed.parameter, + }); + + return localParsed; } catch (error) { - console.error("Failed to apply spell parameters:", error); - return { appliedFilter: null, relays: [] }; + console.error( + `[RelaySpell:${spell.name || spellId}] Failed to parse spell:`, + error, + ); + return null; } - }, [spell.event, targetRelay]); + }, [spell, targetRelay, spellId]); + + // Apply parameters to get final filter + const appliedFilter = useMemo(() => { + if (!parsed || !targetRelay) return null; + + try { + const applied = applySpellParameters(parsed, [targetRelay]); + console.log(`[RelaySpell:${spell.name || spellId}] Applied parameters:`, { + input: targetRelay, + result: applied, + }); + return applied; + } catch (error) { + console.error( + `[RelaySpell:${spell.name || spellId}] Failed to apply parameters:`, + error, + ); + return null; + } + }, [parsed, targetRelay, spell.name, spellId]); + + // Resolve relays - for $relay spells, we query FROM the target relay itself + const finalRelays = useMemo(() => { + // Use explicit relays from spell if provided + if (parsed?.relays && parsed.relays.length > 0) { + console.log( + `[RelaySpell:${spell.name || spellId}] Using explicit relays:`, + parsed.relays, + ); + return parsed.relays; + } + + // For $relay spells, query FROM the target relay + console.log(`[RelaySpell:${spell.name || spellId}] Using target relay:`, [ + targetRelay, + ]); + return [targetRelay]; + }, [parsed?.relays, targetRelay, spell.name, spellId]); // Fetch events using the applied filter const { events, loading, eoseReceived, relayStates, overallState } = - appliedFilter + appliedFilter && finalRelays.length > 0 ? useReqTimelineEnhanced( `spell-${spellId}-${targetRelay}`, appliedFilter, - relays, + finalRelays, { limit: appliedFilter.limit || 50, stream: true }, ) : { @@ -90,6 +176,14 @@ function SpellTabContent({ return map; }, [relayStates]); + console.log(`[RelaySpell:${spell.name || spellId}] Render state:`, { + hasFilter: !!appliedFilter, + relayCount: finalRelays.length, + eventCount: events.length, + loading, + eoseReceived, + }); + return (

Unable to apply spell to this relay

+

Check console for details

) : ( @@ -109,7 +204,7 @@ function SpellTabContent({ loading={loading} overallState={overallState} events={events} - relays={relays} + relays={finalRelays} filter={appliedFilter} spellEvent={spell.event} reqRelayStates={reqRelayStatesMap} diff --git a/src/hooks/useParameterizedSpells.ts b/src/hooks/useParameterizedSpells.ts index a160d7d..89a094a 100644 --- a/src/hooks/useParameterizedSpells.ts +++ b/src/hooks/useParameterizedSpells.ts @@ -105,8 +105,22 @@ export function useParameterizedSpells( // Include spells with $me or $contacts that could be parameterized if (!spell.parameterType) { const cmd = spell.command.toLowerCase(); - // Check if command uses $me or $contacts (single arg that can become $pubkey) - return cmd.includes("$me") || cmd.includes("$contacts"); + // Check if command uses ONLY $me or ONLY $contacts (not both, not mixed with other pubkeys) + const hasMeOrContacts = + cmd.includes("$me") || cmd.includes("$contacts"); + if (!hasMeOrContacts) return false; + + // Make sure it doesn't use multiple different variables + const hasBothMeAndContacts = + cmd.includes("$me") && cmd.includes("$contacts"); + if (hasBothMeAndContacts) return false; // Can't have both + + // Check if it has hex pubkeys mixed with $me/$contacts + // This is a rough check - looking for hex strings in -a or #p tags + const hasHexPubkey = /(?:-a|#p)\s+[a-f0-9]{64}/.test(cmd); + if (hasHexPubkey) return false; // Mixed pubkeys, not eligible + + return true; } return false; diff --git a/src/lib/spell-conversion.ts b/src/lib/spell-conversion.ts index f7f528c..d41a546 100644 --- a/src/lib/spell-conversion.ts +++ b/src/lib/spell-conversion.ts @@ -518,7 +518,7 @@ export function reconstructCommand( * @returns Filter with parameters applied */ export function applySpellParameters( - parsed: ParsedSpell, + parsed: Pick, args: string[] = [], ): NostrFilter { if (!parsed.parameter) {