diff --git a/src/components/NoteCard.tsx b/src/components/NoteCard.tsx index b49b732e..f1c8589a 100644 --- a/src/components/NoteCard.tsx +++ b/src/components/NoteCard.tsx @@ -22,6 +22,8 @@ import { Sparkles, UserCheck, Users, + Volume2, + VolumeX, Zap, } from "lucide-react"; import { nip19 } from "nostr-tools"; @@ -256,22 +258,6 @@ function getTag(tags: string[][], name: string): string | undefined { } /** Parse single imeta tag into structured object (legacy, for kind 34236 vines). */ -function parseImeta(tags: string[][]): { url?: string; thumbnail?: string } { - const imetaTag = tags.find(([name]) => name === "imeta"); - if (!imetaTag) return {}; - const result: Record = {}; - for (let i = 1; i < imetaTag.length; i++) { - const part = imetaTag[i]; - const spaceIdx = part.indexOf(" "); - if (spaceIdx === -1) continue; - const key = part.slice(0, spaceIdx); - const value = part.slice(spaceIdx + 1); - if (key === "url") result.url = value; - else if (key === "image") result.thumbnail = value; - } - return result; -} - /** Encodes the NIP-19 identifier for navigating to an event. */ function encodeEventId(event: NostrEvent): string { // Addressable events (30000-39999) use naddr with their d-tag @@ -391,7 +377,7 @@ export const NoteCard = memo(function NoteCard({ auxOpenPost(e); }; - const isVine = event.kind === 34236; + const isVine = event.kind === 34236 || event.kind === 22; const isPoll = event.kind === 1068; const isGeocache = event.kind === 37516; const isFoundLog = event.kind === 7516; @@ -415,9 +401,7 @@ export const NoteCard = memo(function NoteCard({ const isPollVote = event.kind === 1018; const isRepost = event.kind === 6 || event.kind === 16; const isPhoto = event.kind === 20; - const isNormalVideo = event.kind === 21; - const isShortVideo = event.kind === 22; - const isVideo = isNormalVideo || isShortVideo; + const isVideo = event.kind === 21; const isMusicTrack = event.kind === 36787; const isMusicPlaylist = event.kind === 34139; const isPodcastEpisode = event.kind === 30054; @@ -521,9 +505,9 @@ export const NoteCard = memo(function NoteCard({ }, [event, isReply]); const parentEventId = parentHints?.id; - // Kind 34236 specific + // Kind 22 / 34236 specific const imeta = useMemo( - () => (isVine ? parseImeta(event.tags) : undefined), + () => (isVine ? parseVideoImeta(event.tags) : undefined), [event.tags, isVine], ); const vineTitle = isVine ? getTag(event.tags, "title") : undefined; @@ -1389,7 +1373,10 @@ function VideoContent({ event }: { event: NostrEvent }) { ); } -/** Media content for kind 34236 vine events — rendered at full card width. */ +/** Module-level mute state shared across all VineMedia instances in the feed. */ +let vineGlobalMuted = true; + +/** Media content for kind 22 / 34236 short-form video events — rendered at full card width. */ function VineMedia({ imeta, hashtags, @@ -1400,6 +1387,7 @@ function VineMedia({ const videoRef = useRef(null); const containerRef = useRef(null); const [isPlaying, setIsPlaying] = useState(false); + const [isMuted, setIsMuted] = useState(vineGlobalMuted); // Pause video when scrolled out of view useEffect(() => { @@ -1425,14 +1413,30 @@ function VineMedia({ const video = videoRef.current; if (!video) return; if (video.paused) { - video.play(); - setIsPlaying(true); + // Start muted (required by browsers), then sync to shared state once playing + video.muted = true; + video.play().then(() => { + video.muted = vineGlobalMuted; + setIsMuted(vineGlobalMuted); + }).catch(() => { + // play blocked — leave paused + }); } else { video.pause(); setIsPlaying(false); } }; + const handleMuteToggle = (e: React.MouseEvent) => { + e.stopPropagation(); + const video = videoRef.current; + if (!video) return; + const next = !video.muted; + video.muted = next; + vineGlobalMuted = next; + setIsMuted(next); + }; + return ( <> {imeta?.url && ( @@ -1448,6 +1452,7 @@ function VineMedia({ className="w-full max-h-[70vh] object-cover" loop playsInline + muted preload="none" onPlay={() => setIsPlaying(true)} onPause={() => setIsPlaying(false)} @@ -1459,6 +1464,20 @@ function VineMedia({ )} + {/* Mute/unmute toggle */} + {isPlaying && ( + + )} )}