mirror of
https://github.com/hoornet/vega.git
synced 2026-06-30 14:08:34 -07:00
V4V: keysend payments, recipient breakdown, and episode nudge
- Add payKeysendViaNWC for node pubkey recipients with TLV records - Route V4V payments to keysend or LNURL-pay based on recipient type - Show recipient split breakdown in V4V panel (name + percentage) - Add V4V nudge: brief tooltip when V4V episode starts (once per session) - Highlight V4V button in amber when episode has recipients but streaming off - Enhanced V4V badge in episode list with lightning icon and pill style
This commit is contained in:
@@ -132,7 +132,9 @@ export function EpisodeList({ show, onBack }: EpisodeListProps) {
|
||||
<span className="text-[10px] text-accent">resumed</span>
|
||||
)}
|
||||
{ep.value && ep.value.length > 0 && (
|
||||
<span className="text-[10px] text-amber-500">V4V</span>
|
||||
<span className="text-[10px] text-amber-400 bg-amber-500/10 px-1.5 py-0.5 rounded-sm font-medium">
|
||||
⚡ V4V
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,21 +1,45 @@
|
||||
import { useState, useCallback } from "react";
|
||||
import { useState, useCallback, useEffect, useRef } from "react";
|
||||
import { usePodcastStore } from "../../stores/podcast";
|
||||
import { startStreaming, stopStreaming, boost } from "../../lib/podcast/v4v";
|
||||
|
||||
const RATE_OPTIONS = [5, 10, 21, 50, 100];
|
||||
const NWC_KEY = "wrystr_nwc_uri";
|
||||
|
||||
// Track which episodes have shown the nudge this session (not persisted)
|
||||
const nudgedGuids = new Set<string>();
|
||||
|
||||
export function V4VIndicator() {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [boostAmount, setBoostAmount] = useState("100");
|
||||
const [boosting, setBoosting] = useState(false);
|
||||
const [showNudge, setShowNudge] = useState(false);
|
||||
const nudgeTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
|
||||
const episode = usePodcastStore((s) => s.currentEpisode);
|
||||
const v4vSatsPerMinute = usePodcastStore((s) => s.v4vSatsPerMinute);
|
||||
const v4vStreaming = usePodcastStore((s) => s.v4vStreaming);
|
||||
const v4vTotalStreamed = usePodcastStore((s) => s.v4vTotalStreamed);
|
||||
const playbackState = usePodcastStore((s) => s.playbackState);
|
||||
const { setV4VEnabled, setV4VSatsPerMinute, setV4VStreaming, addStreamedSats } = usePodcastStore.getState();
|
||||
|
||||
// Show nudge when a V4V episode starts playing and streaming is off
|
||||
useEffect(() => {
|
||||
if (
|
||||
playbackState === "playing" &&
|
||||
episode?.value &&
|
||||
episode.value.length > 0 &&
|
||||
!v4vStreaming &&
|
||||
!nudgedGuids.has(episode.guid)
|
||||
) {
|
||||
nudgedGuids.add(episode.guid);
|
||||
setShowNudge(true);
|
||||
nudgeTimer.current = setTimeout(() => setShowNudge(false), 5000);
|
||||
}
|
||||
return () => {
|
||||
if (nudgeTimer.current) clearTimeout(nudgeTimer.current);
|
||||
};
|
||||
}, [playbackState, episode?.guid, v4vStreaming]);
|
||||
|
||||
const nwcUri = localStorage.getItem(NWC_KEY) ?? "";
|
||||
const hasWallet = !!nwcUri;
|
||||
const hasRecipients = episode?.value && episode.value.length > 0;
|
||||
@@ -57,17 +81,30 @@ export function V4VIndicator() {
|
||||
return (
|
||||
<div className="relative shrink-0">
|
||||
<button
|
||||
onClick={() => setOpen(!open)}
|
||||
onClick={() => { setOpen(!open); setShowNudge(false); }}
|
||||
className={`text-[11px] px-1.5 py-0.5 rounded-sm transition-colors ${
|
||||
v4vStreaming
|
||||
? "text-amber-400 bg-amber-500/10 animate-pulse"
|
||||
: "text-text-dim hover:text-text"
|
||||
: hasRecipients && !v4vStreaming
|
||||
? "text-amber-400 bg-amber-500/10 hover:bg-amber-500/20"
|
||||
: "text-text-dim hover:text-text"
|
||||
}`}
|
||||
title="Value 4 Value"
|
||||
>
|
||||
{v4vStreaming ? `${v4vTotalStreamed} sats` : "V4V"}
|
||||
{v4vStreaming ? `⚡ ${v4vTotalStreamed} sats` : hasRecipients ? "⚡ V4V" : "V4V"}
|
||||
</button>
|
||||
|
||||
{/* Brief nudge when V4V episode starts — once per episode per session */}
|
||||
{showNudge && (
|
||||
<div
|
||||
className="absolute bottom-full right-0 mb-2 px-3 py-2 bg-amber-500/15 border border-amber-500/30 rounded-sm text-[10px] text-amber-300 whitespace-nowrap z-50 animate-fade-in"
|
||||
onClick={() => { setShowNudge(false); setOpen(true); }}
|
||||
style={{ cursor: "pointer" }}
|
||||
>
|
||||
⚡ This episode supports V4V — stream sats to the creators
|
||||
</div>
|
||||
)}
|
||||
|
||||
{open && (
|
||||
<div className="absolute bottom-full right-0 mb-2 w-56 bg-bg border border-border rounded-sm shadow-lg p-3 z-50">
|
||||
<div className="text-[11px] text-text font-medium mb-2">Value 4 Value</div>
|
||||
@@ -122,6 +159,23 @@ export function V4VIndicator() {
|
||||
<span className="text-[9px] text-text-dim">/min</span>
|
||||
</div>
|
||||
|
||||
{/* Recipients */}
|
||||
{episode.value && episode.value.length > 0 && (
|
||||
<div className="mb-2 border-t border-border pt-2">
|
||||
<div className="text-[9px] text-text-dim mb-1">Sats go to:</div>
|
||||
{episode.value.map((r, i) => {
|
||||
const totalSplit = episode.value!.reduce((s, v) => s + v.split, 0);
|
||||
const pct = totalSplit > 0 ? Math.round((r.split / totalSplit) * 100) : 0;
|
||||
return (
|
||||
<div key={i} className="flex items-center justify-between text-[9px]">
|
||||
<span className="text-text-muted truncate">{r.name || r.address?.slice(0, 12) + "…"}</span>
|
||||
<span className="text-text-dim shrink-0 ml-1">{pct}%</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Boost */}
|
||||
<div className="flex items-center gap-1 border-t border-border pt-2">
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user