From 551275c5058ba31d55c3e55d96928cb1190eed95 Mon Sep 17 00:00:00 2001 From: highperfocused Date: Wed, 29 Oct 2025 20:58:11 +0100 Subject: [PATCH] feat: implement zap revenue splits functionality across article and blog post forms --- src/components/ArticleView.tsx | 29 ++++++++ src/components/ProfessionalBlogPostForm.tsx | 79 ++++++++++++++++++++- src/config.tsx | 15 ++++ src/hooks/usePublishBlogPost.ts | 32 +++++++++ src/hooks/useZaps.ts | 15 ++++ 5 files changed, 169 insertions(+), 1 deletion(-) create mode 100644 src/config.tsx diff --git a/src/components/ArticleView.tsx b/src/components/ArticleView.tsx index 3c1ccba..9a4b9a6 100644 --- a/src/components/ArticleView.tsx +++ b/src/components/ArticleView.tsx @@ -58,6 +58,21 @@ export function ArticleView({ post }: ArticleViewProps) { .filter(([name]) => name === 't') .map(([, value]) => value); + // Parse zap splits from tags (community convention under NIP-57) + const zapSplits = post.tags + .filter(([n]) => n === 'zap') + .map((t) => { + const pub = t[1] ?? ''; + let weight = 0; + const idx = t.findIndex((x) => x === 'weight'); + if (idx >= 0 && t[idx + 1]) weight = parseInt(t[idx + 1] as string) || 0; + else if (t[2]) { + const n = parseInt(t[2] as string); if (!Number.isNaN(n)) weight = n; + } + return { pubkey: pub, weight }; + }) + .filter((s) => s.pubkey && s.weight > 0); + const date = publishedAt ? new Date(parseInt(publishedAt) * 1000) : new Date(post.created_at * 1000); @@ -202,6 +217,20 @@ export function ArticleView({ post }: ArticleViewProps) { + {zapSplits.length > 0 && ( +
+
Zap revenue splits
+
+ {zapSplits.map((s, i) => ( + +
{genUserName(s.pubkey)}
+ {s.weight}% + + ))} +
+
+ )} +
)} + + {/* Zap revenue split (fixed recipient, adjustable percentage) */} + {HOUSE_SPLIT_ENABLED && ( +
+ +
+ setSplitPercent(v[0] ?? 0)} + /> +
+ {splitPercent}% goes to the platform. {100 - splitPercent}% goes to you. +
+
+
+ )} )} diff --git a/src/config.tsx b/src/config.tsx new file mode 100644 index 0000000..c4b9a1e --- /dev/null +++ b/src/config.tsx @@ -0,0 +1,15 @@ +// Global app configuration +// Central place to store static keys and app-level constants + +// Hex-encoded public key for platform revenue share (house account) +// Note: Keep this in hex to avoid decoding at runtime. +export const HOUSE_PUBKEY_HEX = + '1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef'; + +// Toggle showing and applying the house zap split on the article creation page +export const HOUSE_SPLIT_ENABLED = true; + +export default { + HOUSE_PUBKEY_HEX, + HOUSE_SPLIT_ENABLED, +}; diff --git a/src/hooks/usePublishBlogPost.ts b/src/hooks/usePublishBlogPost.ts index 26fdeab..936b287 100644 --- a/src/hooks/usePublishBlogPost.ts +++ b/src/hooks/usePublishBlogPost.ts @@ -11,6 +11,12 @@ interface BlogPostData { content: string; hashtags?: string[]; publishedAt?: number; + /** Optional zap revenue splits. Weights are integers (percent). */ + splits?: Array<{ + pubkey: string; // hex or npub1 + weight: number; // 1..100 + relays?: string[]; + }>; } /** @@ -51,6 +57,32 @@ export function usePublishBlogPost() { }); } + // Zap splits (NIP-57 community convention): add one `zap` tag per recipient. + // Tag shape we emit: ["zap", , "weight", "", "relays", , ...] + if (data.splits && data.splits.length > 0) { + for (const split of data.splits) { + // Normalize pubkey: accept npub or hex; if npub, decode to hex + let hexPub = split.pubkey; + try { + if (hexPub.startsWith('npub1')) { + const { nip19 } = await import('nostr-tools'); + const decoded = nip19.decode(hexPub); + if (decoded.type === 'npub') { + hexPub = decoded.data as string; + } + } + } catch { + // keep as-is; signer/relays may ignore invalid entries + } + + const parts: string[] = ['zap', hexPub, 'weight', String(Math.max(0, Math.min(100, Math.trunc(split.weight || 0))))]; + if (split.relays && split.relays.length > 0) { + parts.push('relays', ...split.relays); + } + tags.push(parts); + } + } + // Add client tag // tags.push(['client', 'zelo.news']); diff --git a/src/hooks/useZaps.ts b/src/hooks/useZaps.ts index 4ed7162..7a2db7e 100644 --- a/src/hooks/useZaps.ts +++ b/src/hooks/useZaps.ts @@ -209,6 +209,21 @@ export function useZaps( comment }); + // If target event contains zap split tags, include them in the zap request, + // so compatible zap servers can honor the split distribution. + try { + if (Array.isArray(actualTarget.tags)) { + const splitTags = actualTarget.tags.filter((t) => t[0] === 'zap'); + if (splitTags.length > 0) { + const zr = zapRequest as unknown as { tags?: string[][] }; + zr.tags = Array.isArray(zr.tags) ? zr.tags : []; + zr.tags.push(...splitTags); + } + } + } catch (e) { + console.debug('Failed to include zap split tags in zap request', e); + } + // Sign the zap request (but don't publish to relays - only send to LNURL endpoint) if (!user.signer) { throw new Error('No signer available');