import { useEffect, useState, useMemo, useRef, useCallback } from 'react'; import { MapContainer, TileLayer, CircleMarker, Popup, useMap } from 'react-leaflet'; import type { LatLngBoundsExpression, CircleMarker as LeafletCircleMarker } from 'leaflet'; import 'leaflet/dist/leaflet.css'; import type { Contact } from '../types'; import { formatTime } from '../utils/messageParser'; import { isValidLocation } from '../utils/pathUtils'; import { CONTACT_TYPE_REPEATER } from '../types'; interface MapViewProps { contacts: Contact[]; /** Public key of contact to focus on and open popup */ focusedKey?: string | null; } // Calculate marker color based on how recently the contact was heard function getMarkerColor(lastSeen: number): string { const now = Date.now() / 1000; const age = now - lastSeen; const hour = 3600; const day = 86400; if (age < hour) return '#22c55e'; // Bright green - less than 1 hour if (age < day) return '#4ade80'; // Light green - less than 1 day if (age < 3 * day) return '#a3e635'; // Yellow-green - less than 3 days return '#9ca3af'; // Gray - older (up to 7 days) } // Component to handle map bounds fitting function MapBoundsHandler({ contacts, focusedContact, }: { contacts: Contact[]; focusedContact: Contact | null; }) { const map = useMap(); const [hasInitialized, setHasInitialized] = useState(false); useEffect(() => { // If we have a focused contact, center on it immediately (even if already initialized) if (focusedContact && focusedContact.lat != null && focusedContact.lon != null) { map.setView([focusedContact.lat, focusedContact.lon], 12); setHasInitialized(true); return; } if (hasInitialized) return; const fitToContacts = () => { if (contacts.length === 0) { // No contacts with location - show world view map.setView([20, 0], 2); setHasInitialized(true); return; } if (contacts.length === 1) { // Single contact - center on it map.setView([contacts[0].lat!, contacts[0].lon!], 10); setHasInitialized(true); return; } // Multiple contacts - fit bounds const bounds: LatLngBoundsExpression = contacts.map( (c) => [c.lat!, c.lon!] as [number, number] ); map.fitBounds(bounds, { padding: [50, 50], maxZoom: 12 }); setHasInitialized(true); }; // Try geolocation first if ('geolocation' in navigator) { navigator.geolocation.getCurrentPosition( (position) => { // Success - center on user location with reasonable zoom map.setView([position.coords.latitude, position.coords.longitude], 8); setHasInitialized(true); }, () => { // Geolocation denied/failed - fit to contacts fitToContacts(); }, { timeout: 5000, maximumAge: 300000 } ); } else { // No geolocation support - fit to contacts fitToContacts(); } }, [map, contacts, hasInitialized, focusedContact]); return null; } export function MapView({ contacts, focusedKey }: MapViewProps) { // Filter to contacts with GPS coordinates, heard within the last 7 days. // Always include the focused contact so "view on map" links work for older nodes. const mappableContacts = useMemo(() => { const sevenDaysAgo = Date.now() / 1000 - 7 * 24 * 60 * 60; return contacts.filter( (c) => isValidLocation(c.lat, c.lon) && (c.public_key === focusedKey || (c.last_seen != null && c.last_seen > sevenDaysAgo)) ); }, [contacts, focusedKey]); // Find the focused contact by key const focusedContact = useMemo(() => { if (!focusedKey) return null; return mappableContacts.find((c) => c.public_key === focusedKey) || null; }, [focusedKey, mappableContacts]); // Track marker refs to open popup programmatically const markerRefs = useRef>({}); // Store ref for a marker const setMarkerRef = useCallback((key: string, ref: LeafletCircleMarker | null) => { markerRefs.current[key] = ref; }, []); // Open popup for focused contact after map is ready useEffect(() => { if (focusedContact && markerRefs.current[focusedContact.public_key]) { // Small delay to ensure map has finished rendering const timer = setTimeout(() => { markerRefs.current[focusedContact.public_key]?.openPopup(); }, 100); return () => clearTimeout(timer); } }, [focusedContact]); return (
{/* Info bar */}
Showing {mappableContacts.length} contact{mappableContacts.length !== 1 ? 's' : ''} heard in the last 7 days
{/* Map - z-index constrained to stay below modals/sheets */}
{mappableContacts.map((contact) => { const isRepeater = contact.type === CONTACT_TYPE_REPEATER; const color = getMarkerColor(contact.last_seen!); const displayName = contact.name || contact.public_key.slice(0, 12); return ( setMarkerRef(contact.public_key, ref)} center={[contact.lat!, contact.lon!]} radius={isRepeater ? 10 : 7} pathOptions={{ color: isRepeater ? color : '#000', fillColor: color, fillOpacity: 0.8, weight: isRepeater ? 0 : 1, }} >
{isRepeater && ( )} {displayName}
Last heard: {formatTime(contact.last_seen!)}
{contact.lat!.toFixed(5)}, {contact.lon!.toFixed(5)}
); })}
); }