From 599e8b6c6033b6bac8ffd80c09092b95581feaaa Mon Sep 17 00:00:00 2001 From: Alejandro Date: Mon, 19 Jan 2026 11:41:55 +0100 Subject: [PATCH] Fix zap dialog to target correct event author (#152) * fix: use semantic author for zap targeting When zapping certain event kinds (zaps, streams), use the semantic author instead of event.pubkey: - Zaps (9735): Target the zapper, not the lightning service - Streams (30311): Target the host, not the event publisher Changes: - Extract getSemanticAuthor() to shared utility (src/lib/semantic-author.ts) - Update BaseEventRenderer to use semantic author when opening zap dialog - Update ZapWindow to resolve recipient using semantic author - Refactor DynamicWindowTitle to use shared utility This ensures that when you zap an event, you're zapping the right person (the one who semantically "owns" or created the event), not just whoever signed it. * fix: load event in DynamicWindowTitle to derive zap recipient When opening a zap dialog via 'zap naddr1...' or 'zap nevent1...', the window title was showing "ZAP" instead of "Zap {host name}" because DynamicWindowTitle only had access to the empty recipientPubkey from the initial props. Now DynamicWindowTitle: - Loads the event from eventPointer if present - Derives the recipient using getSemanticAuthor() if recipientPubkey is empty - Falls back to explicit recipientPubkey if provided This ensures the window title shows the correct recipient name immediately, matching the behavior in the ZapWindow component itself. --------- Co-authored-by: Claude --- src/components/DynamicWindowTitle.tsx | 46 +++++++------------ src/components/ZapWindow.tsx | 7 ++- .../nostr/kinds/BaseEventRenderer.tsx | 6 ++- src/lib/semantic-author.ts | 41 +++++++++++++++++ 4 files changed, 67 insertions(+), 33 deletions(-) create mode 100644 src/lib/semantic-author.ts diff --git a/src/components/DynamicWindowTitle.tsx b/src/components/DynamicWindowTitle.tsx index 834bed7..0ab9d65 100644 --- a/src/components/DynamicWindowTitle.tsx +++ b/src/components/DynamicWindowTitle.tsx @@ -23,9 +23,7 @@ import { import { getEventDisplayTitle } from "@/lib/event-title"; import { UserName } from "./nostr/UserName"; import { getTagValues } from "@/lib/nostr-utils"; -import { getLiveHost } from "@/lib/live-activity"; -import type { NostrEvent } from "@/types/nostr"; -import { getZapSender } from "applesauce-common/helpers/zap"; +import { getSemanticAuthor } from "@/lib/semantic-author"; // import { NipC7Adapter } from "@/lib/chat/adapters/nip-c7-adapter"; // Coming soon import { Nip29Adapter } from "@/lib/chat/adapters/nip-29-adapter"; import type { ChatProtocol, ProtocolIdentifier } from "@/types/chat"; @@ -37,31 +35,6 @@ export interface WindowTitleData { tooltip?: string; } -/** - * Get the semantic author of an event based on kind-specific logic - * Returns the pubkey that should be displayed as the "author" for UI purposes - * - * Examples: - * - Zaps (9735): Returns the zapper (P tag), not the lightning service pubkey - * - Live activities (30311): Returns the host (first p tag with "Host" role) - * - Regular events: Returns event.pubkey - */ -function getSemanticAuthor(event: NostrEvent): string { - switch (event.kind) { - case 9735: { - // Zap: show the zapper, not the lightning service pubkey - const zapSender = getZapSender(event); - return zapSender || event.pubkey; - } - case 30311: { - // Live activity: show the host - return getLiveHost(event); - } - default: - return event.pubkey; - } -} - /** * Format profile names with prefix, handling $me and $contacts aliases * @param prefix - Prefix to use (e.g., 'by ', '@ ') @@ -474,8 +447,21 @@ function useDynamicTitle(window: WindowInstance): WindowTitleData { const countHashtags = appId === "count" && props.filter?.["#t"] ? props.filter["#t"] : []; - // Zap titles - const zapRecipientPubkey = appId === "zap" ? props.recipientPubkey : null; + // Zap titles - load event to derive recipient if needed + const zapEventPointer: EventPointer | AddressPointer | undefined = + appId === "zap" ? props.eventPointer : undefined; + const zapEvent = useNostrEvent(zapEventPointer); + + // Derive recipient: use explicit pubkey or semantic author from event + const zapRecipientPubkey = useMemo(() => { + if (appId !== "zap") return null; + // If explicit recipient provided, use it + if (props.recipientPubkey) return props.recipientPubkey; + // Otherwise derive from event's semantic author + if (zapEvent) return getSemanticAuthor(zapEvent); + return null; + }, [appId, props.recipientPubkey, zapEvent]); + const zapRecipientProfile = useProfile(zapRecipientPubkey || ""); const zapTitle = useMemo(() => { if (appId !== "zap" || !zapRecipientPubkey) return null; diff --git a/src/components/ZapWindow.tsx b/src/components/ZapWindow.tsx index 2580eff..aea9b09 100644 --- a/src/components/ZapWindow.tsx +++ b/src/components/ZapWindow.tsx @@ -49,6 +49,7 @@ import { } from "@/lib/create-zap-request"; import { fetchInvoiceFromCallback } from "@/lib/lnurl"; import { useLnurlCache } from "@/hooks/useLnurlCache"; +import { getSemanticAuthor } from "@/lib/semantic-author"; export interface ZapWindowProps { /** Recipient pubkey (who receives the zap) */ @@ -98,8 +99,10 @@ export function ZapWindow({ ); }, [eventPointer]); - // Resolve recipient: use provided pubkey or derive from event author - const recipientPubkey = initialRecipientPubkey || event?.pubkey || ""; + // Resolve recipient: use provided pubkey or derive from semantic author + // For zaps, this returns the zapper; for streams, returns the host; etc. + const recipientPubkey = + initialRecipientPubkey || (event ? getSemanticAuthor(event) : ""); const recipientProfile = useProfile(recipientPubkey); diff --git a/src/components/nostr/kinds/BaseEventRenderer.tsx b/src/components/nostr/kinds/BaseEventRenderer.tsx index 9b4986a..1c43ca3 100644 --- a/src/components/nostr/kinds/BaseEventRenderer.tsx +++ b/src/components/nostr/kinds/BaseEventRenderer.tsx @@ -21,6 +21,7 @@ import { getSeenRelays } from "applesauce-core/helpers/relays"; import { EventFooter } from "@/components/EventFooter"; import { cn } from "@/lib/utils"; import { isAddressableKind } from "@/lib/nostr-kinds"; +import { getSemanticAuthor } from "@/lib/semantic-author"; /** * Universal event properties and utilities shared across all kind renderers @@ -173,9 +174,12 @@ export function EventMenu({ event }: { event: NostrEvent }) { }; } + // Get semantic author (e.g., zapper for zaps, host for streams) + const recipientPubkey = getSemanticAuthor(event); + // Open zap window with event context addWindow("zap", { - recipientPubkey: event.pubkey, + recipientPubkey, eventPointer, }); }; diff --git a/src/lib/semantic-author.ts b/src/lib/semantic-author.ts new file mode 100644 index 0000000..b28a2b3 --- /dev/null +++ b/src/lib/semantic-author.ts @@ -0,0 +1,41 @@ +/** + * Semantic Author Utilities + * + * Determines the "semantic author" of an event based on kind-specific logic. + * For most events, this is event.pubkey, but for certain event types the + * semantic author may be different (e.g., zapper for zaps, host for streams). + */ + +import type { NostrEvent } from "@/types/nostr"; +import { getZapSender } from "applesauce-common/helpers/zap"; +import { getLiveHost } from "@/lib/live-activity"; + +/** + * Get the semantic author of an event based on kind-specific logic + * Returns the pubkey that should be displayed as the "author" for UI purposes + * + * Examples: + * - Zaps (9735): Returns the zapper (P tag), not the lightning service pubkey + * - Live activities (30311): Returns the host (first p tag with "Host" role) + * - Regular events: Returns event.pubkey + * + * This function should be used when determining: + * - Who to display as the author in UI + * - Who to zap when zapping an event + * - Who the "owner" of the event is semantically + */ +export function getSemanticAuthor(event: NostrEvent): string { + switch (event.kind) { + case 9735: { + // Zap: show the zapper, not the lightning service pubkey + const zapSender = getZapSender(event); + return zapSender || event.pubkey; + } + case 30311: { + // Live activity: show the host + return getLiveHost(event); + } + default: + return event.pubkey; + } +}