From ebf964980b693b7af71998eb6a01641deee8e049 Mon Sep 17 00:00:00 2001 From: Jure <44338+hoornet@users.noreply.github.com> Date: Sat, 4 Apr 2026 19:03:00 +0200 Subject: [PATCH] Add V4V sidebar section with dashboard, settings, and history - New V4V store with budget tracking, cap enforcement, and history - Dashboard: live streaming status, per-episode + weekly budget bars, stats - Settings: auto-enable toggle (guarded by caps), per-episode cap, weekly budget, default rate, info icons - History: expandable list of past V4V episodes with recipient breakdowns - Budget enforcement: streaming stops with toast when cap is hit - Auto-streaming: starts automatically for V4V episodes when enabled - Cap reached state: dashboard shows red card, ticker shows "(capped)" - V4VIndicator: slimmed popup, AUTO badge, "open v4v" nav link - Fix duplicate history entries on cap stop --- src/App.tsx | 2 + src/components/podcast/V4VIndicator.tsx | 103 ++++++++++------ src/components/sidebar/Sidebar.tsx | 1 + src/components/v4v/V4VDashboard.tsx | 128 ++++++++++++++++++++ src/components/v4v/V4VHistory.tsx | 79 +++++++++++++ src/components/v4v/V4VSettings.tsx | 133 +++++++++++++++++++++ src/components/v4v/V4VView.tsx | 44 +++++++ src/lib/podcast/v4v.ts | 56 ++++++++- src/stores/ui.ts | 2 +- src/stores/v4v.ts | 150 ++++++++++++++++++++++++ 10 files changed, 659 insertions(+), 39 deletions(-) create mode 100644 src/components/v4v/V4VDashboard.tsx create mode 100644 src/components/v4v/V4VHistory.tsx create mode 100644 src/components/v4v/V4VSettings.tsx create mode 100644 src/components/v4v/V4VView.tsx create mode 100644 src/stores/v4v.ts diff --git a/src/App.tsx b/src/App.tsx index 3ac4d93..a31d474 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -24,6 +24,7 @@ const BookmarkView = lazy(() => import("./components/bookmark/BookmarkView").the const HashtagFeed = lazy(() => import("./components/feed/HashtagFeed").then(m => ({ default: m.HashtagFeed }))); const PodcastsView = lazy(() => import("./components/podcast/PodcastsView").then(m => ({ default: m.PodcastsView }))); const FollowsView = lazy(() => import("./components/follows/FollowsView").then(m => ({ default: m.FollowsView }))); +const V4VView = lazy(() => import("./components/v4v/V4VView").then(m => ({ default: m.V4VView }))); const DebugPanel = lazy(() => import("./components/shared/DebugPanel").then(m => ({ default: m.DebugPanel }))); const HelpModal = lazy(() => import("./components/shared/HelpModal").then(m => ({ default: m.HelpModal }))); import { useUIStore } from "./stores/ui"; @@ -131,6 +132,7 @@ function App() { {currentView === "hashtag" && } {currentView === "podcasts" && } {currentView === "follows" && } + {currentView === "v4v" && } diff --git a/src/components/podcast/V4VIndicator.tsx b/src/components/podcast/V4VIndicator.tsx index a456ca6..a2a4568 100644 --- a/src/components/podcast/V4VIndicator.tsx +++ b/src/components/podcast/V4VIndicator.tsx @@ -1,8 +1,9 @@ import { useState, useCallback, useEffect, useRef } from "react"; import { usePodcastStore } from "../../stores/podcast"; +import { useV4VStore } from "../../stores/v4v"; +import { useUIStore } from "../../stores/ui"; 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) @@ -20,15 +21,47 @@ export function V4VIndicator() { 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(); + const { setV4VEnabled, setV4VStreaming, addStreamedSats } = usePodcastStore.getState(); - // Show nudge when a V4V episode starts playing and streaming is off + const autoEnabled = useV4VStore((s) => s.autoEnabled); + const defaultRate = useV4VStore((s) => s.defaultRate); + const capReachedReason = useV4VStore((s) => s.capReachedReason); + + const nwcUri = localStorage.getItem(NWC_KEY) ?? ""; + const hasWallet = !!nwcUri; + const hasRecipients = episode?.value && episode.value.length > 0; + + // Auto-start streaming when autoEnabled and V4V episode starts playing + useEffect(() => { + if ( + autoEnabled && + playbackState === "playing" && + hasRecipients && + hasWallet && + !v4vStreaming && + episode + ) { + const intervalId = startStreaming( + episode, + defaultRate, + nwcUri, + (amount) => addStreamedSats(amount), + ); + if (intervalId >= 0) { + setV4VStreaming(true, intervalId); + setV4VEnabled(true); + } + } + }, [playbackState, episode?.guid, autoEnabled]); + + // Show nudge when a V4V episode starts playing and streaming is off (manual mode) useEffect(() => { if ( playbackState === "playing" && episode?.value && episode.value.length > 0 && !v4vStreaming && + !autoEnabled && !nudgedGuids.has(episode.guid) ) { nudgedGuids.add(episode.guid); @@ -38,11 +71,7 @@ export function V4VIndicator() { 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; + }, [playbackState, episode?.guid, v4vStreaming, autoEnabled]); const toggleStreaming = useCallback(() => { if (!episode || !hasWallet) return; @@ -52,9 +81,10 @@ export function V4VIndicator() { setV4VStreaming(false); setV4VEnabled(false); } else { + const rate = autoEnabled ? defaultRate : v4vSatsPerMinute; const intervalId = startStreaming( episode, - v4vSatsPerMinute, + rate, nwcUri, (amount) => addStreamedSats(amount), ); @@ -63,7 +93,7 @@ export function V4VIndicator() { setV4VEnabled(true); } } - }, [episode, v4vStreaming, v4vSatsPerMinute, nwcUri, hasWallet]); + }, [episode, v4vStreaming, v4vSatsPerMinute, defaultRate, autoEnabled, nwcUri, hasWallet]); const handleBoost = useCallback(async () => { if (!episode || !hasWallet || boosting) return; @@ -82,16 +112,26 @@ export function V4VIndicator() {
{/* Brief nudge when V4V episode starts — once per episode per session */} @@ -107,7 +147,15 @@ export function V4VIndicator() { {open && (
-
Value 4 Value
+
+
Value 4 Value
+ +
{!hasWallet && (
@@ -140,25 +188,6 @@ export function V4VIndicator() {
- {/* Rate picker */} -
- Rate: - {RATE_OPTIONS.map((rate) => ( - - ))} - /min -
- {/* Recipients */} {episode.value && episode.value.length > 0 && (
diff --git a/src/components/sidebar/Sidebar.tsx b/src/components/sidebar/Sidebar.tsx index 626df60..aa0f25d 100644 --- a/src/components/sidebar/Sidebar.tsx +++ b/src/components/sidebar/Sidebar.tsx @@ -18,6 +18,7 @@ const NAV_ITEMS = [ { id: "notifications" as const, label: "notifications", icon: "🔔" }, { id: "follows" as const, label: "follows", icon: "♺" }, { id: "zaps" as const, label: "zaps", icon: "⚡" }, + { id: "v4v" as const, label: "v4v", icon: "⚡" }, { id: "relays" as const, label: "relays", icon: "⟐" }, { id: "settings" as const, label: "settings", icon: "⚙" }, { id: "about" as const, label: "support", icon: "♥" }, diff --git a/src/components/v4v/V4VDashboard.tsx b/src/components/v4v/V4VDashboard.tsx new file mode 100644 index 0000000..f46e878 --- /dev/null +++ b/src/components/v4v/V4VDashboard.tsx @@ -0,0 +1,128 @@ +import { usePodcastStore } from "../../stores/podcast"; +import { useV4VStore } from "../../stores/v4v"; + +function BudgetBar({ label, spent, total }: { label: string; spent: number; total: number }) { + if (total <= 0) return null; + const pct = Math.min(100, Math.round((spent / total) * 100)); + const isNear = pct >= 80; + const isOver = pct >= 100; + + return ( +
+
+ {label} + + {spent} / {total} sats + +
+
+
+
+
+ {total - spent > 0 ? `${total - spent} sats remaining` : "Budget reached"} +
+
+ ); +} + +export function V4VDashboard() { + const episode = usePodcastStore((s) => s.currentEpisode); + const v4vStreaming = usePodcastStore((s) => s.v4vStreaming); + const v4vTotalStreamed = usePodcastStore((s) => s.v4vTotalStreamed); + + const autoEnabled = useV4VStore((s) => s.autoEnabled); + const perEpisodeCap = useV4VStore((s) => s.perEpisodeCap); + const weeklyBudget = useV4VStore((s) => s.weeklyBudget); + const defaultRate = useV4VStore((s) => s.defaultRate); + const currentEpisodeSats = useV4VStore((s) => s.currentEpisodeSats); + const capReachedReason = useV4VStore((s) => s.capReachedReason); + const history = useV4VStore((s) => s.history); + const weeklySpent = useV4VStore((s) => s.weeklySpent)(); + + const hasRecipients = episode?.value && episode.value.length > 0; + + // All-time stats + const allTimeSats = history.reduce((sum, e) => sum + e.satsStreamed + e.satsBoosted, 0) + v4vTotalStreamed; + const episodesSupported = new Set(history.map((e) => e.episodeGuid)).size; + + return ( +
+ {/* Current streaming status */} +
+

Now

+ + {!episode ? ( +
No episode playing.
+ ) : !hasRecipients ? ( +
+ Playing {episode.title} — no V4V recipients. +
+ ) : v4vStreaming ? ( +
+
+ + Streaming + {autoEnabled && ( + AUTO + )} +
+
{episode.title}
+
+ {v4vTotalStreamed} sats this session · {episode.value?.length} recipients +
+
+ ) : capReachedReason ? ( +
+
+ + {capReachedReason} +
+
{episode.title}
+
+ {currentEpisodeSats} sats streamed to {episode.value?.length} recipients +
+
+ ) : ( +
+ Playing {episode.title} — V4V available but not streaming. +
+ )} +
+ + {/* Budget bars */} + {(perEpisodeCap > 0 || weeklyBudget > 0) && ( +
+

Budget

+ + +
+ )} + + {/* Quick stats */} +
+

Stats

+
+
+
{allTimeSats}
+
total sats
+
+
+
{episodesSupported}
+
episodes
+
+
+
+ {autoEnabled ? `${defaultRate}/m` : "off"} +
+
auto-stream
+
+
+
+
+ ); +} diff --git a/src/components/v4v/V4VHistory.tsx b/src/components/v4v/V4VHistory.tsx new file mode 100644 index 0000000..21ed00e --- /dev/null +++ b/src/components/v4v/V4VHistory.tsx @@ -0,0 +1,79 @@ +import { useState } from "react"; +import { useV4VStore, type V4VHistoryEntry } from "../../stores/v4v"; + +function formatDate(ts: number): string { + const d = new Date(ts); + return d.toLocaleDateString(undefined, { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit" }); +} + +function HistoryRow({ entry }: { entry: V4VHistoryEntry }) { + const [expanded, setExpanded] = useState(false); + const totalSats = entry.satsStreamed + entry.satsBoosted; + + return ( +
+ + + {expanded && ( +
+ {entry.satsStreamed > 0 && ( +
Streamed: {entry.satsStreamed} sats
+ )} + {entry.satsBoosted > 0 && ( +
Boosted: {entry.satsBoosted} sats
+ )} + {entry.recipients.length > 0 && ( +
+
Recipients:
+ {entry.recipients.map((r, i) => ( +
+ {r.name || r.address.slice(0, 16) + "..."} + {r.sats} sats +
+ ))} +
+ )} +
+ )} +
+ ); +} + +export function V4VHistory() { + const history = useV4VStore((s) => s.history); + + if (history.length === 0) { + return ( +
+
+
No V4V history yet
+
+ Start streaming to see your contributions here. +
+
+
+ ); + } + + return ( +
+ {history.map((entry, i) => ( + + ))} +
+ ); +} diff --git a/src/components/v4v/V4VSettings.tsx b/src/components/v4v/V4VSettings.tsx new file mode 100644 index 0000000..b05c265 --- /dev/null +++ b/src/components/v4v/V4VSettings.tsx @@ -0,0 +1,133 @@ +import { useV4VStore } from "../../stores/v4v"; + +const RATE_OPTIONS = [5, 10, 21, 50, 100]; + +function InfoIcon({ title }: { title: string }) { + return ( + + ⓘ + + ); +} + +export function V4VSettings() { + const autoEnabled = useV4VStore((s) => s.autoEnabled); + const perEpisodeCap = useV4VStore((s) => s.perEpisodeCap); + const weeklyBudget = useV4VStore((s) => s.weeklyBudget); + const defaultRate = useV4VStore((s) => s.defaultRate); + const { + setAutoEnabled, setPerEpisodeCap, setWeeklyBudget, setDefaultRate, + } = useV4VStore.getState(); + + return ( +
+ {/* Auto-enable */} +
+
+
+ Auto-stream + +
+ +
+

+ Automatically stream sats to creators when playing V4V-enabled episodes. +

+ {autoEnabled && ( +
+ Auto-streaming is active +
+ )} +
+ + {/* Per-episode cap */} +
+
+ + +
+

+ Stop streaming after this many sats per episode. +

+
+ setPerEpisodeCap(parseInt(e.target.value) || 0)} + placeholder="e.g. 100" + className="w-24 bg-bg-raised border border-border rounded-sm px-2 py-1 text-[11px] text-text placeholder:text-text-dim/50" + /> + sats +
+
+ + {/* Weekly budget */} +
+
+ + +
+

+ Total sats allowed per week across all episodes. +

+
+ setWeeklyBudget(parseInt(e.target.value) || 0)} + placeholder="e.g. 500" + className="w-24 bg-bg-raised border border-border rounded-sm px-2 py-1 text-[11px] text-text placeholder:text-text-dim/50" + /> + sats / week +
+
+ + {/* Default rate */} +
+
+ + +
+

+ Sats per minute when auto-streaming. +

+
+ {RATE_OPTIONS.map((rate) => ( + + ))} + /min +
+
+ + {/* Requirements notice */} + {!autoEnabled && (perEpisodeCap <= 0 || weeklyBudget <= 0) && ( +
+ Set both a per-episode cap and weekly budget to enable auto-streaming. +
+ )} +
+ ); +} diff --git a/src/components/v4v/V4VView.tsx b/src/components/v4v/V4VView.tsx new file mode 100644 index 0000000..42c2b1e --- /dev/null +++ b/src/components/v4v/V4VView.tsx @@ -0,0 +1,44 @@ +import { useState } from "react"; +import { V4VDashboard } from "./V4VDashboard"; +import { V4VSettings } from "./V4VSettings"; +import { V4VHistory } from "./V4VHistory"; + +const TABS = ["dashboard", "settings", "history"] as const; +type Tab = (typeof TABS)[number]; + +export function V4VView() { + const [tab, setTab] = useState("dashboard"); + + return ( +
+ {/* Header */} +
+
+

Value 4 Value

+
+ {TABS.map((t) => ( + + ))} +
+
+
+ + {/* Content */} +
+ {tab === "dashboard" && } + {tab === "settings" && } + {tab === "history" && } +
+
+ ); +} diff --git a/src/lib/podcast/v4v.ts b/src/lib/podcast/v4v.ts index 12fc404..8f2a27c 100644 --- a/src/lib/podcast/v4v.ts +++ b/src/lib/podcast/v4v.ts @@ -1,6 +1,8 @@ import { fetch } from "@tauri-apps/plugin-http"; import type { PodcastEpisode, V4VRecipient } from "../../types/podcast"; import { payInvoiceViaNWC, payKeysendViaNWC } from "../lightning/nwc"; +import { useV4VStore } from "../../stores/v4v"; +import { useToastStore } from "../../stores/toast"; const LNURL_CACHE: Record = {}; @@ -74,6 +76,8 @@ async function payRecipient( let streamingInterval: number | null = null; let accumulatedSats = 0; let accumulatedMinutes = 0; +let currentStreamingEpisode: PodcastEpisode | null = null; +let sessionRecipientSats: Record = {}; export function startStreaming( episode: PodcastEpisode, @@ -85,14 +89,31 @@ export function startStreaming( accumulatedSats = 0; accumulatedMinutes = 0; + currentStreamingEpisode = episode; + sessionRecipientSats = {}; const recipients = getRecipients(episode); if (recipients.length === 0) return -1; + const v4vStore = useV4VStore.getState(); + v4vStore.resetCurrentEpisodeSats(); + // Normalize splits to sum to 100 const totalSplit = recipients.reduce((sum, r) => sum + r.split, 0); streamingInterval = window.setInterval(async () => { + // Check budget caps before accumulating + const v4v = useV4VStore.getState(); + if (v4v.isCapReached()) { + const reason = v4v.perEpisodeCap > 0 && v4v.currentEpisodeSats >= v4v.perEpisodeCap + ? "Per-episode cap reached" + : "Weekly budget reached"; + useToastStore.getState().addToast(reason, "warning", 6000); + v4v.setCapReachedReason(reason); + stopStreaming(); + return; + } + accumulatedMinutes += 1; accumulatedSats += satsPerMinute; @@ -110,7 +131,12 @@ export function startStreaming( try { const success = await payRecipient(recipient, amountMsats, nwcUri); - if (success) onPayment(recipientSats); + if (success) { + onPayment(recipientSats); + useV4VStore.getState().addCurrentEpisodeSats(recipientSats); + const key = recipient.name || recipient.address || "unknown"; + sessionRecipientSats[key] = (sessionRecipientSats[key] || 0) + recipientSats; + } } catch { // Payment failed — silently continue } @@ -120,13 +146,41 @@ export function startStreaming( return streamingInterval; } +function recordHistory() { + const episode = currentStreamingEpisode; + if (!episode) return; + + const v4v = useV4VStore.getState(); + const totalStreamed = v4v.currentEpisodeSats; + if (totalStreamed <= 0) return; + + const recipients = Object.entries(sessionRecipientSats).map(([name, sats]) => ({ + name, + address: "", + sats, + })); + + v4v.addHistoryEntry({ + episodeGuid: episode.guid, + episodeTitle: episode.title, + showTitle: episode.showTitle || "", + satsStreamed: totalStreamed, + satsBoosted: 0, + recipients, + timestamp: Date.now(), + }); +} + export function stopStreaming() { if (streamingInterval !== null) { + recordHistory(); clearInterval(streamingInterval); streamingInterval = null; } accumulatedSats = 0; accumulatedMinutes = 0; + currentStreamingEpisode = null; + sessionRecipientSats = {}; } export async function boost( diff --git a/src/stores/ui.ts b/src/stores/ui.ts index 27f4af9..b9cb46a 100644 --- a/src/stores/ui.ts +++ b/src/stores/ui.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { NDKEvent } from "@nostr-dev-kit/ndk"; -type View = "feed" | "search" | "relays" | "settings" | "profile" | "thread" | "article-editor" | "article" | "articles" | "media" | "podcasts" | "about" | "zaps" | "dm" | "notifications" | "bookmarks" | "hashtag" | "follows"; +type View = "feed" | "search" | "relays" | "settings" | "profile" | "thread" | "article-editor" | "article" | "articles" | "media" | "podcasts" | "about" | "zaps" | "dm" | "notifications" | "bookmarks" | "hashtag" | "follows" | "v4v"; type FeedTab = "global" | "following" | "trending"; interface ViewStackEntry { diff --git a/src/stores/v4v.ts b/src/stores/v4v.ts new file mode 100644 index 0000000..8ad9b97 --- /dev/null +++ b/src/stores/v4v.ts @@ -0,0 +1,150 @@ +import { create } from "zustand"; +import { useToastStore } from "./toast"; + +const STORAGE_KEY = "wrystr_v4v"; +const MAX_HISTORY = 500; +const WEEK_MS = 7 * 24 * 60 * 60 * 1000; + +export interface V4VHistoryEntry { + episodeGuid: string; + episodeTitle: string; + showTitle: string; + satsStreamed: number; + satsBoosted: number; + recipients: { name: string; address: string; sats: number }[]; + timestamp: number; +} + +interface V4VPersistedState { + autoEnabled: boolean; + perEpisodeCap: number; + weeklyBudget: number; + defaultRate: number; + history: V4VHistoryEntry[]; +} + +interface V4VState extends V4VPersistedState { + currentEpisodeSats: number; + capReachedReason: string | null; // Set when a cap stops streaming + + // Computed + weeklySpent: () => number; + weeklyRemaining: () => number; + isCapReached: () => boolean; + + // Actions + setAutoEnabled: (v: boolean) => void; + setPerEpisodeCap: (v: number) => void; + setWeeklyBudget: (v: number) => void; + setDefaultRate: (v: number) => void; + setCapReachedReason: (reason: string | null) => void; + addHistoryEntry: (entry: V4VHistoryEntry) => void; + addCurrentEpisodeSats: (amount: number) => void; + resetCurrentEpisodeSats: () => void; +} + +function loadPersisted(): V4VPersistedState { + try { + const raw = localStorage.getItem(STORAGE_KEY); + if (!raw) return { autoEnabled: false, perEpisodeCap: 0, weeklyBudget: 0, defaultRate: 10, history: [] }; + return JSON.parse(raw); + } catch { + return { autoEnabled: false, perEpisodeCap: 0, weeklyBudget: 0, defaultRate: 10, history: [] }; + } +} + +function persist(state: V4VPersistedState) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); + } catch { /* ignore */ } +} + +function getPersistable(s: V4VState): V4VPersistedState { + return { + autoEnabled: s.autoEnabled, + perEpisodeCap: s.perEpisodeCap, + weeklyBudget: s.weeklyBudget, + defaultRate: s.defaultRate, + history: s.history, + }; +} + +const initial = loadPersisted(); + +export const useV4VStore = create((set, get) => ({ + ...initial, + currentEpisodeSats: 0, + capReachedReason: null, + + weeklySpent: () => { + const cutoff = Date.now() - WEEK_MS; + return get().history + .filter((e) => e.timestamp > cutoff) + .reduce((sum, e) => sum + e.satsStreamed + e.satsBoosted, 0); + }, + + weeklyRemaining: () => { + const { weeklyBudget } = get(); + if (weeklyBudget <= 0) return Infinity; + return Math.max(0, weeklyBudget - get().weeklySpent()); + }, + + isCapReached: () => { + const { perEpisodeCap, currentEpisodeSats } = get(); + if (perEpisodeCap > 0 && currentEpisodeSats >= perEpisodeCap) return true; + if (get().weeklyRemaining() <= 0) return true; + return false; + }, + + setAutoEnabled: (v) => { + const { perEpisodeCap, weeklyBudget } = get(); + if (v && (perEpisodeCap <= 0 || weeklyBudget <= 0)) { + useToastStore.getState().addToast( + "Set per-episode cap and weekly budget before enabling auto-stream", + "warning", + ); + return; + } + set({ autoEnabled: v }); + persist(getPersistable({ ...get(), autoEnabled: v })); + }, + + setPerEpisodeCap: (v) => { + set({ perEpisodeCap: v }); + const s = { ...get(), perEpisodeCap: v }; + if (s.autoEnabled && v <= 0) { + set({ autoEnabled: false }); + s.autoEnabled = false; + } + persist(getPersistable(s)); + }, + + setWeeklyBudget: (v) => { + set({ weeklyBudget: v }); + const s = { ...get(), weeklyBudget: v }; + if (s.autoEnabled && v <= 0) { + set({ autoEnabled: false }); + s.autoEnabled = false; + } + persist(getPersistable(s)); + }, + + setDefaultRate: (v) => { + set({ defaultRate: v }); + persist(getPersistable({ ...get(), defaultRate: v })); + }, + + setCapReachedReason: (reason) => set({ capReachedReason: reason }), + + addHistoryEntry: (entry) => { + const history = [entry, ...get().history].slice(0, MAX_HISTORY); + set({ history }); + persist(getPersistable({ ...get(), history })); + }, + + addCurrentEpisodeSats: (amount) => { + set((s) => ({ currentEpisodeSats: s.currentEpisodeSats + amount })); + }, + + resetCurrentEpisodeSats: () => set({ currentEpisodeSats: 0, capReachedReason: null }), +}));