From ede8aa28e0cbc058c5e9c904a45d0731062dba0c Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Sun, 1 Mar 2026 13:28:01 -0600 Subject: [PATCH 1/3] fix(vines): correct mobile height for safe-area-inset and preload adjacent videos MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add .vine-slide-height CSS utility that subtracts env(safe-area-inset-top/bottom) so slides don't bleed under the bottom nav on PWA/APK with home indicator - Replace all hardcoded calc(100dvh-9.5rem) vine heights with the new utility - Preload active video with 'auto', adjacent (±1) with 'metadata', and far-away cards with 'none' to speed up video playback on swipe without wasting mobile bandwidth --- src/index.css | 5 +++++ src/pages/VinesFeedPage.tsx | 15 +++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/index.css b/src/index.css index 3bad5086..465c757d 100644 --- a/src/index.css +++ b/src/index.css @@ -49,6 +49,11 @@ .top-mobile-bar { top: calc(3rem + env(safe-area-inset-top, 0px)); } + + /* Vine feed slide height: full viewport minus top bar, tab bar, bottom nav, and safe-area insets */ + .vine-slide-height { + height: calc(100dvh - env(safe-area-inset-top, 0px) - 3rem - 2.5rem - 3.5rem - env(safe-area-inset-bottom, 0px)); + } } diff --git a/src/pages/VinesFeedPage.tsx b/src/pages/VinesFeedPage.tsx index e9f1efb3..e3f57027 100644 --- a/src/pages/VinesFeedPage.tsx +++ b/src/pages/VinesFeedPage.tsx @@ -303,10 +303,12 @@ function VineHeartButton({ event, label }: { event: NostrEvent; label?: string } interface VineCardProps { event: NostrEvent; isActive: boolean; + /** True for the card immediately before or after the active one — used to preload video. */ + isNearActive: boolean; onCommentClick: () => void; } -function VineCard({ event, isActive, onCommentClick }: VineCardProps) { +function VineCard({ event, isActive, isNearActive, onCommentClick }: VineCardProps) { const { user } = useCurrentUser(); const author = useAuthor(event.pubkey); const metadata = author.data?.metadata; @@ -382,7 +384,7 @@ function VineCard({ event, isActive, onCommentClick }: VineCardProps) { loop playsInline muted={isMuted} - preload="metadata" + preload={isActive ? 'auto' : isNearActive ? 'metadata' : 'none'} onPlay={() => { setIsPlaying(true); setHasStarted(true); setIsAttemptingPlay(false); }} onPause={() => { setIsPlaying(false); setIsAttemptingPlay(false); }} onError={onBlossomError} @@ -707,7 +709,7 @@ export function VinesFeedPage() { // ── Loading state ──────────────────────────────────────────────────────── if (isLoading) { return ( -
+
{/* Vine card skeleton */}
@@ -733,7 +735,7 @@ export function VinesFeedPage() { // ── Empty state ────────────────────────────────────────────────────────── if (!isLoading && vines.length === 0) { return ( -
+
@@ -766,17 +768,18 @@ export function VinesFeedPage() {
{vines.map((event, i) => (
From e5a7a7c121ccda957faa5a539fe7bf8de833a1ec Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Sun, 1 Mar 2026 13:30:23 -0600 Subject: [PATCH 2/3] fix(vines): remove double-subtraction of tab bar height in loading/empty states --- src/pages/VinesFeedPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pages/VinesFeedPage.tsx b/src/pages/VinesFeedPage.tsx index e3f57027..b7e30171 100644 --- a/src/pages/VinesFeedPage.tsx +++ b/src/pages/VinesFeedPage.tsx @@ -709,7 +709,7 @@ export function VinesFeedPage() { // ── Loading state ──────────────────────────────────────────────────────── if (isLoading) { return ( -
+
{/* Vine card skeleton */}
@@ -735,7 +735,7 @@ export function VinesFeedPage() { // ── Empty state ────────────────────────────────────────────────────────── if (!isLoading && vines.length === 0) { return ( -
+
From 4bae1cdf31d63d30dbf05a04c84d5df8caabcd75 Mon Sep 17 00:00:00 2001 From: Chad Curtis Date: Sun, 1 Mar 2026 13:41:49 -0600 Subject: [PATCH 3/3] fix(layout): remove min-h-screen from main wrapper to prevent outer-page scroll on vines --- src/components/MainLayout.tsx | 2 +- src/pages/VinesFeedPage.tsx | 66 ++++++++++++++--------------------- 2 files changed, 27 insertions(+), 41 deletions(-) diff --git a/src/components/MainLayout.tsx b/src/components/MainLayout.tsx index 3f2d259e..9828497c 100644 --- a/src/components/MainLayout.tsx +++ b/src/components/MainLayout.tsx @@ -80,7 +80,7 @@ function MainLayoutInner() { {/* Main layout - three column on desktop */} -
+
{/* Desktop left sidebar - hidden below sidebar breakpoint */}
diff --git a/src/pages/VinesFeedPage.tsx b/src/pages/VinesFeedPage.tsx index b7e30171..9587f708 100644 --- a/src/pages/VinesFeedPage.tsx +++ b/src/pages/VinesFeedPage.tsx @@ -624,11 +624,9 @@ export function VinesFeedPage() { [events], ); - // Reset active index when tab or vine list changes significantly + // Reset active index when tab changes useEffect(() => { setActiveIndex(0); - const container = containerRef.current; - if (container) container.scrollTop = 0; }, [tab]); const activeVine = vines[activeIndex]; @@ -656,20 +654,10 @@ export function VinesFeedPage() { }; }, [commentsOpen]); - // Snap-scroll to a specific vine index - const scrollToIndex = useCallback((index: number) => { - const container = containerRef.current; - if (!container) return; - const target = container.children[index] as HTMLElement | undefined; - if (!target) return; - target.scrollIntoView({ behavior: 'smooth', block: 'start' }); - }, []); - - // Update active index based on intersection + // Sync activeIndex when CSS snap settles (touch swipe, mouse wheel, trackpad) useEffect(() => { const container = containerRef.current; if (!container) return; - const observer = new IntersectionObserver( (entries) => { for (const entry of entries) { @@ -681,30 +669,30 @@ export function VinesFeedPage() { }, { root: container, threshold: 0.5 }, ); - - const kids = Array.from(container.children); - kids.forEach((child) => observer.observe(child)); + Array.from(container.children).forEach((child) => observer.observe(child)); return () => observer.disconnect(); }, [vines]); // Keyboard arrow navigation useEffect(() => { const handleKey = (e: KeyboardEvent) => { + const container = containerRef.current; + if (!container) return; if (e.key === 'ArrowDown') { e.preventDefault(); const next = Math.min(activeIndex + 1, vines.length - 1); + container.scrollTo({ top: next * container.clientHeight, behavior: 'smooth' }); setActiveIndex(next); - scrollToIndex(next); } else if (e.key === 'ArrowUp') { e.preventDefault(); const prev = Math.max(activeIndex - 1, 0); + container.scrollTo({ top: prev * container.clientHeight, behavior: 'smooth' }); setActiveIndex(prev); - scrollToIndex(prev); } }; window.addEventListener('keydown', handleKey); return () => window.removeEventListener('keydown', handleKey); - }, [activeIndex, vines.length, scrollToIndex]); + }, [activeIndex, vines.length]); // ── Loading state ──────────────────────────────────────────────────────── if (isLoading) { @@ -765,26 +753,24 @@ export function VinesFeedPage() { {/* ── Scroll container ────────────────────────────────────────── */} -
-
- {vines.map((event, i) => ( -
- -
- ))} -
+
+ {vines.map((event, i) => ( +
+ +
+ ))}
{/* ── Mobile comments panel — full overlay, xl:hidden ─────────── */}