import { useState, useEffect, useRef } from 'react' import axios from 'axios' import VCardPreview from './VCardPreview' import './LazyMedia.css' const API_BASE = import.meta.env.VITE_API_URL || 'http://localhost:8081/api' function LazyMedia({ messageId, mediaType, className, alt = "MMS attachment" }) { const [src, setSrc] = useState(null) const [vcfData, setVcfData] = useState(null) const [loading, setLoading] = useState(false) const [error, setError] = useState(false) const [showModal, setShowModal] = useState(false) const imgRef = useRef(null) const videoRef = useRef(null) const observerRef = useRef(null) const hasLoadedRef = useRef(false) useEffect(() => { // Reset loaded state when messageId changes hasLoadedRef.current = false // Set up Intersection Observer for lazy loading observerRef.current = new IntersectionObserver( (entries) => { entries.forEach((entry) => { if (entry.isIntersecting && !hasLoadedRef.current) { hasLoadedRef.current = true loadMedia() } }) }, { // Only load images below viewport (not above) to prevent scroll jump // rootMargin: top right bottom left rootMargin: '50px 0px 200px 0px' } ) if (imgRef.current) { observerRef.current.observe(imgRef.current) } // Load media before printing to ensure all images are available const handleBeforePrint = () => { if (!hasLoadedRef.current) { hasLoadedRef.current = true loadMedia() } } window.addEventListener('beforeprint', handleBeforePrint) return () => { if (observerRef.current) { observerRef.current.disconnect() } window.removeEventListener('beforeprint', handleBeforePrint) } }, [messageId]) const loadMedia = async () => { setLoading(true) try { // Check if this is a VCF file - fetch as text instead of blob const isVCard = mediaType === 'text/x-vcard' || mediaType === 'text/vcard' || mediaType === 'text/directory' if (isVCard) { // Fetch VCF as text const response = await axios.get(`${API_BASE}/media`, { params: { id: messageId }, responseType: 'text' }) setVcfData(response.data) } else { // Fetch other media as blob const response = await axios.get(`${API_BASE}/media`, { params: { id: messageId }, responseType: 'blob' }) const blob = response.data const objectUrl = URL.createObjectURL(blob) setSrc(objectUrl) } // Stop observing once loaded - we don't need to track this element anymore if (observerRef.current && imgRef.current) { observerRef.current.unobserve(imgRef.current) } } catch (err) { console.error('Failed to load media:', err) setError(true) } finally { setLoading(false) } } // Cleanup object URL when component unmounts useEffect(() => { return () => { if (src) { URL.revokeObjectURL(src) } } }, [src]) // Handle ESC key to close modal useEffect(() => { const handleEscape = (e) => { if (e.key === 'Escape' && showModal) { setShowModal(false) } } if (showModal) { document.addEventListener('keydown', handleEscape) // Prevent body scroll when modal is open document.body.style.overflow = 'hidden' // Hide UI elements that might appear over the full-screen modal const datePickers = document.querySelectorAll('.react-datepicker-popper') datePickers.forEach(picker => { picker.style.display = 'none' }) const dateFilterContainer = document.querySelector('.date-filter-container') if (dateFilterContainer) { dateFilterContainer.style.visibility = 'hidden' } // Hide the page header const header = document.querySelector('header') if (header) { header.style.visibility = 'hidden' } } return () => { document.removeEventListener('keydown', handleEscape) document.body.style.overflow = 'unset' // Restore visibility of hidden elements const datePickers = document.querySelectorAll('.react-datepicker-popper') datePickers.forEach(picker => { picker.style.display = '' }) const dateFilterContainer = document.querySelector('.date-filter-container') if (dateFilterContainer) { dateFilterContainer.style.visibility = '' } const header = document.querySelector('header') if (header) { header.style.visibility = '' } } }, [showModal]) // Pause original video when modal opens useEffect(() => { if (showModal && videoRef.current) { videoRef.current.pause() } }, [showModal]) if (!mediaType) { return null } const isImage = mediaType.startsWith('image/') const isVideo = mediaType.startsWith('video/') const isAudio = mediaType.startsWith('audio/') const isVCard = mediaType === 'text/x-vcard' || mediaType === 'text/vcard' || mediaType === 'text/directory' return ( <>