fix mobile pwa header video startup and slide overflow

This commit is contained in:
Avery Felts 2026-02-24 02:40:36 -07:00
parent e8f47993a9
commit 95e60594e8
3 changed files with 90 additions and 10 deletions

View File

@ -619,7 +619,7 @@
scroll-snap-type: x mandatory;
scroll-behavior: smooth;
-webkit-overflow-scrolling: touch;
touch-action: pan-x pinch-zoom;
touch-action: pan-x pan-y pinch-zoom;
scrollbar-width: none;
-ms-overflow-style: none;
gap: 1rem;
@ -643,6 +643,7 @@
display: flex;
flex-direction: column;
justify-content: flex-start;
padding-bottom: calc(env(safe-area-inset-bottom) + 7.25rem);
overflow: visible;
}
}

View File

@ -309,14 +309,19 @@ export function UnifiedQuitPlanCard({
if (!showNicotine && !showWeed) return null;
return (
<Card className="backdrop-blur-2xl shadow-2xl border-white/10 overflow-hidden bg-white/5">
<Card
className={cn(
"backdrop-blur-2xl shadow-2xl border-white/10 overflow-hidden bg-white/5",
!isDesktopVariant && "max-h-[calc(100dvh-13.5rem)]"
)}
>
<CardHeader className="pb-1 pt-6 px-4 sm:px-6">
<CardTitle className="flex items-center gap-2 text-sm sm:text-base font-black uppercase tracking-widest opacity-60">
<TrendingDown className="h-4 w-4 text-primary" />
Quit Journey Plan
</CardTitle>
</CardHeader>
<CardContent className="pt-2 p-2 sm:p-4">
<CardContent className={cn("pt-2 p-2 sm:p-4", !isDesktopVariant && "overflow-y-auto overscroll-contain pr-1")}>
{showNicotine && (
<SubstancePlanSection
substance="nicotine"

View File

@ -140,6 +140,7 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader
const { theme } = useTheme();
const { isSupported, permission, requestPermission } = useNotifications(reminderSettings);
const videoRef = useRef<HTMLVideoElement>(null);
const videoRetryTimeoutRef = useRef<number | null>(null);
const [isVideoLoaded, setIsVideoLoaded] = useState(false);
const [isVideoPlaying, setIsVideoPlaying] = useState(false);
@ -160,6 +161,35 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader
checkMobile();
}, []);
const clearVideoRetryTimeout = () => {
if (videoRetryTimeoutRef.current !== null) {
window.clearTimeout(videoRetryTimeoutRef.current);
videoRetryTimeoutRef.current = null;
}
};
const scheduleVideoRetry = () => {
clearVideoRetryTimeout();
videoRetryTimeoutRef.current = window.setTimeout(() => {
const video = videoRef.current;
if (!video) return;
if (video.readyState < 2) {
video.load();
}
video.play().catch(() => {
// Ignore autoplay retries that still fail.
});
}, 900);
};
useEffect(() => {
return () => {
clearVideoRetryTimeout();
};
}, []);
// Force play background video
useEffect(() => {
if (videoRef.current) {
@ -176,12 +206,15 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader
video.playsInline = true;
video.setAttribute('playsinline', 'true'); // Explicit attribute for some older browsers
video.setAttribute('webkit-playsinline', 'true');
video.preload = 'metadata';
} else {
video.preload = 'auto';
}
video.loop = true;
// If already ready, set loaded immediately
if (video.readyState >= 3) {
if (video.readyState >= 2) {
setIsVideoLoaded(true);
}
@ -190,10 +223,20 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader
if (playPromise !== undefined) {
playPromise.catch((err) => {
console.warn("Autoplay failed, user interaction might be needed:", err);
// If PWA, we might want to show the poster as fallback silently without error spam
if (isMobile || isPWA) {
scheduleVideoRetry();
}
});
}
if (isMobile || isPWA) {
scheduleVideoRetry();
}
}
return () => {
clearVideoRetryTimeout();
};
}, [isMobile, isPWA]);
useEffect(() => {
@ -366,18 +409,49 @@ export function UserHeader({ user, preferences, onModalStateChange }: UserHeader
"x-webkit-airplay": "deny",
"disableRemotePlayback": true
} as any)}
preload="auto"
preload={isMobile || isPWA ? 'metadata' : 'auto'}
controls={false}
disablePictureInPicture
onContextMenu={(e) => e.preventDefault()}
onLoadedData={() => setIsVideoLoaded(true)}
onPlay={() => setIsVideoPlaying(true)}
onPlaying={() => setIsVideoPlaying(true)}
onTimeUpdate={() => {
if (!isVideoPlaying && videoRef.current && videoRef.current.currentTime > 0) {
onCanPlay={() => {
setIsVideoLoaded(true);
if (videoRef.current?.paused) {
videoRef.current.play().catch(() => {
if (isMobile || isPWA) {
scheduleVideoRetry();
}
});
}
}}
onPlaying={() => {
if (videoRef.current && videoRef.current.currentTime > 0.06) {
setIsVideoPlaying(true);
}
}}
onTimeUpdate={() => {
if (!isVideoPlaying && videoRef.current && videoRef.current.currentTime > 0.06) {
setIsVideoPlaying(true);
}
}}
onWaiting={() => {
setIsVideoPlaying(false);
if (isMobile || isPWA) {
scheduleVideoRetry();
}
}}
onStalled={() => {
setIsVideoPlaying(false);
if (isMobile || isPWA) {
scheduleVideoRetry();
}
}}
onPause={() => {
setIsVideoPlaying(false);
}}
onError={() => {
setIsVideoPlaying(false);
}}
className={cn(
"absolute inset-0 w-full h-full object-cover transition-opacity duration-[1500ms] ease-in-out",
isMobile ? "scale-105" : "scale-110",