refactor(map): share the marker thumbnail loading between the Leaflet and GL maps

MapView and MapViewGL each held the same photo loading effect: read the
cached thumbs, fetch the missing ones, listen for their arrival and batch
them into one animation frame. It now lives in useMarkerThumbs, which
also owns the thumb state and the place photos switch, and both maps
call it where the effect used to be.

The effect keeps its keys (the picture sources and the switch). It reads
the places through useEffectEvent, which drops the old
exhaustive-deps suppression in MapViewGL and the warning in MapView.
This commit is contained in:
Maurice
2026-10-09 15:01:16 +02:00
parent 69d2f80fe2
commit 03cb9fa439
4 changed files with 223 additions and 128 deletions
+3 -67
View File
@@ -603,10 +603,8 @@ function MapContextMenuHandler({ onContextMenu }: { onContextMenu: ((e: L.Leafle
// Travel times are shown in the day sidebar (per-segment connectors), not on the map.
// Module-level photo cache shared with PlaceAvatar
import { getCached, isLoading, fetchPhoto, onThumbReady, getAllThumbs } from '../../services/photoService'
import { isCustomPlaceImage, markerPhotoHtml, photoCacheKey, photoSourcesKey, placePhotoFull, placePhotoUrl } from './placePhoto'
import { useAuthStore } from '../../store/authStore'
import { useMarkerThumbs } from './useMarkerThumbs'
import { markerPhotoHtml, placePhotoFull, placePhotoUrl } from './placePhoto'
import { useGeolocation } from '../../hooks/useGeolocation'
import LocationButton from './LocationButton'
import { useIsPhone } from '../../mobile/useIsPhone'
@@ -971,72 +969,10 @@ export const MapView = memo(function MapView({
setTooltipPos(null)
}, [])
// photoUrls: only base64 thumbs for smooth map zoom
const [photoUrls, setPhotoUrls] = useState<Record<string, string>>(getAllThumbs)
const placesPhotosEnabled = useAuthStore(s => s.placesPhotosEnabled)
// Batch photo state updates through a RAF so N simultaneous photo loads
// collapse into a single re-render instead of N separate renders.
const pendingThumbsRef = useRef<Record<string, string>>({})
const thumbRafRef = useRef<number | null>(null)
const photoSources = useMemo(() => photoSourcesKey(places), [places])
// Flattened [lat,lng] points of the selected day's route, so the bounds fit can
// include the full polyline once it has been computed.
const routeCoords = useMemo<[number, number][]>(() => (route || []).flat() as [number, number][], [route])
useEffect(() => {
if (!places || places.length === 0 || !placesPhotosEnabled) return
const cleanups: (() => void)[] = []
const setThumb = (cacheKey: string, thumb: string) => {
pendingThumbsRef.current[cacheKey] = thumb
if (thumbRafRef.current !== null) return
thumbRafRef.current = requestAnimationFrame(() => {
thumbRafRef.current = null
const pending = pendingThumbsRef.current
pendingThumbsRef.current = {}
setPhotoUrls(prev => {
const hasChange = Object.entries(pending).some(([k, v]) => prev[k] !== v)
return hasChange ? { ...prev, ...pending } : prev
})
})
}
for (const place of places) {
// A custom uploaded image is shown directly — never auto-fetch a provider
// photo for it (the request would 404 for OSM-only places and the fetched
// thumb would shadow the user's own image). (#1136)
if (isCustomPlaceImage(place.image_url)) continue
const cacheKey = photoCacheKey(place)
if (!cacheKey) continue
const cached = getCached(cacheKey)
if (cached?.thumbDataUrl) {
setThumb(cacheKey, cached.thumbDataUrl)
continue
}
cleanups.push(onThumbReady(cacheKey, thumb => setThumb(cacheKey, thumb)))
if (!cached && !isLoading(cacheKey)) {
const photoId =
(place.image_url?.startsWith('/api/maps/place-photo/') ? place.image_url : null)
|| place.google_place_id
|| place.osm_id
|| place.image_url
if (photoId || (place.lat && place.lng)) {
fetchPhoto(cacheKey, photoId || `coords:${place.lat}:${place.lng}`, place.lat, place.lng, place.name)
}
}
}
return () => {
cleanups.forEach(fn => fn())
if (thumbRafRef.current !== null) {
cancelAnimationFrame(thumbRafRef.current)
thumbRafRef.current = null
}
}
}, [photoSources, placesPhotosEnabled])
const photoUrls = useMarkerThumbs(places)
const isTouchDevice = typeof window !== 'undefined' && navigator.maxTouchPoints > 0
// Drag a marker onto a day (#891). Pointer-driven, so it is off wherever