Add podcast playback, Fountain.fm cards, V4V streaming, fix notifications

Podcast feature:
- Podcast discovery via Podcast Index API (trending + search)
- Persistent player bar with play/pause, seek, speed (1x/1.5x/2x), volume
- Audio persists across view navigation, resumes from saved position
- Fountain.fm URL detection in feed with rich playable cards
- "Play in Wrystr" button on inline audio blocks
- V4V streaming sats via NWC (LNURL-pay, 5min accumulation, split payments)
- Share what you're listening to (publish note with confirm)
- Space key toggles play/pause globally

Notification fixes:
- Per-notification read tracking (click to mark read) instead of mark-all-on-open
- Read notifications persist at 50% opacity, unread get accent border
- Always fetches last 7 days, keeps 15 most recent
- Filter out own replies from notifications
- Sidebar badge shows only unread count
This commit is contained in:
Jure
2026-03-21 12:53:05 +01:00
parent 1dafb3b456
commit 04180cf186
20 changed files with 1474 additions and 29 deletions
@@ -1,4 +1,4 @@
import { useEffect, useRef } from "react";
import { useEffect } from "react";
import { useUserStore } from "../../stores/user";
import { useMuteStore } from "../../stores/mute";
import { useNotificationsStore } from "../../stores/notifications";
@@ -10,24 +10,20 @@ export function NotificationsView() {
const {
notifications,
unreadCount,
lastSeenAt,
loading,
fetchNotifications,
markAllRead,
markRead,
isRead,
} = useNotificationsStore();
const { mutedPubkeys, contentMatchesMutedKeyword } = useMuteStore();
const filteredNotifications = notifications.filter(
(e) => !mutedPubkeys.includes(e.pubkey) && !contentMatchesMutedKeyword(e.content)
(e) => e.pubkey !== pubkey && !mutedPubkeys.includes(e.pubkey) && !contentMatchesMutedKeyword(e.content)
);
// Capture lastSeenAt at mount time so unread highlights persist during this view session
const prevLastSeenAtRef = useRef(lastSeenAt);
useEffect(() => {
if (!pubkey) return;
fetchNotifications(pubkey).then(() => {
setTimeout(() => markAllRead(), 500);
});
fetchNotifications(pubkey);
}, [pubkey]);
if (!loggedIn || !pubkey) {
@@ -65,11 +61,12 @@ export function NotificationsView() {
)}
{filteredNotifications.map((event) => {
const isUnread = (event.created_at ?? 0) > prevLastSeenAtRef.current;
const read = isRead(event.id!);
return (
<div
key={event.id}
className={isUnread ? "border-l-2 border-accent/40" : ""}
className={`transition-opacity ${read ? "opacity-50" : "border-l-2 border-accent/40"}`}
onClick={() => { if (!read && event.id) markRead(event.id); }}
>
<NoteCard event={event} />
</div>