diff --git a/src/components/ChatBox.tsx b/src/components/ChatBox.tsx index 0806903..1b5ddd1 100644 --- a/src/components/ChatBox.tsx +++ b/src/components/ChatBox.tsx @@ -75,10 +75,11 @@ export default function ChatBox({ showAllMessagesTab = false, className = "", st const res = await fetch(buildApiUrl(url)); const data = await res.json(); if (Array.isArray(data)) { - if (after && data.length > 0) { + if (after) { // Add newer messages to the beginning (most recent first) - setMessages((prev) => [...data, ...prev]); - // Don't update hasMore or lastBefore for auto-refresh + if (data.length > 0) { + setMessages((prev) => [...data, ...prev]); + } } else { setMessages((prev) => replace ? data : [...prev, ...data]); setHasMore(data.length === PAGE_SIZE); diff --git a/src/components/MapIcons.tsx b/src/components/MapIcons.tsx index 574c1c3..e7d1602 100644 --- a/src/components/MapIcons.tsx +++ b/src/components/MapIcons.tsx @@ -1,5 +1,7 @@ import React from 'react'; import moment from "moment"; +import { formatPublicKey } from '../lib/meshcore'; +import { getNameIconLabel } from '../lib/meshcore-map-nodeutils'; type NodePosition = { node_id: string; @@ -39,7 +41,9 @@ export function NodeMarker({ node, showNodeNames = true }: NodeMarkerProps) { return (
{showNodeNames && node.short_name && ( -
{node.short_name}
+
+ {node.type === "meshcore" ? getNameIconLabel(node.name || node.short_name) : node.short_name} +
)}
@@ -144,9 +148,9 @@ export function ClusterMarker({ children }: ClusterMarkerProps) { export function PopupContent({ node }: PopupContentProps) { return (
-
ID: {node.node_id}
+
ID: {node.type === "meshcore" ? formatPublicKey(node.node_id) : node.node_id}
Full Name: {node.name ?? "-"}
-
Short Name: {node.short_name ?? "-"}
+
Short Name: {node.type === "meshcore" && node.short_name ? getNameIconLabel(node.name || node.short_name) : (node.short_name ?? "-")}
Type: {node.type ?? "-"}
Lat: {node.latitude}
Lng: {node.longitude}
diff --git a/src/components/MapView.tsx b/src/components/MapView.tsx index bcbd860..a5739f5 100644 --- a/src/components/MapView.tsx +++ b/src/components/MapView.tsx @@ -31,76 +31,137 @@ type NodePosition = { }; type ClusteredMarkersProps = { nodes: NodePosition[] }; -function ClusteredMarkers({ nodes }: ClusteredMarkersProps) { + +// Individual marker component +function IndividualMarker({ node, showNodeNames }: { node: NodePosition; showNodeNames: boolean }) { const map = useMap(); - const configResult = useConfig(); - const config = configResult?.config; + const markerRef = useRef(null); + useEffect(() => { if (!map) return; - // Remove any previous layers - map.eachLayer((layer: any) => { - if (layer && layer._isClusterLayer) { - map.removeLayer(layer); - } + + const icon = L.divIcon({ + className: 'custom-node-marker-container', + iconSize: [16, 32], + iconAnchor: [8, 8], + html: renderToString(), }); - if (config?.clustering === false) { - // Add markers individually - const markerLayers: any[] = []; - nodes.forEach((node: NodePosition) => { - const icon = L.divIcon({ - className: 'custom-node-marker-container', - iconSize: [16, 32], - iconAnchor: [8, 8], - html: renderToString(), - }); - const marker = L.marker([node.latitude, node.longitude], { icon }); - (marker as any).options.nodeData = node; - marker.bindPopup(renderToString()); - marker.addTo(map); - markerLayers.push(marker); + + const marker = L.marker([node.latitude, node.longitude], { icon }); + (marker as any).options.nodeData = node; + marker.bindPopup(renderToString()); + marker.addTo(map); + markerRef.current = marker; + + return () => { + if (markerRef.current && map.hasLayer(markerRef.current)) { + map.removeLayer(markerRef.current); + } + }; + }, [map, node.latitude, node.longitude, node.node_id, showNodeNames]); + + // Update marker when node data changes + useEffect(() => { + if (markerRef.current) { + const currentPos = markerRef.current.getLatLng(); + if (currentPos.lat !== node.latitude || currentPos.lng !== node.longitude) { + markerRef.current.setLatLng([node.latitude, node.longitude]); + } + + // Update icon and popup + const icon = L.divIcon({ + className: 'custom-node-marker-container', + iconSize: [16, 32], + iconAnchor: [8, 8], + html: renderToString(), }); - // Mark for cleanup - markerLayers.forEach(layer => { layer._isClusterLayer = true; }); - return () => { - markerLayers.forEach(layer => map.removeLayer(layer)); - }; - } else { - // Clustered mode (existing logic) - const iconCreateFunction = (cluster: any) => { - const children = cluster.getAllChildMarkers(); - return L.divIcon({ - html: renderToString({children}), - className: 'custom-cluster-icon', - iconSize: [40, 40], - iconAnchor: [20, 20], - }); - }; - const markers = (L as any).markerClusterGroup({ - iconCreateFunction, - maxClusterRadius: 40, - }); - nodes.forEach((node: NodePosition) => { - const icon = L.divIcon({ - className: 'custom-node-marker-container', - iconSize: [16, 32], - iconAnchor: [8, 8], - html: renderToString(), - }); - const marker = L.marker([node.latitude, node.longitude], { icon }); - (marker as any).options.nodeData = node; - marker.bindPopup(renderToString()); - markers.addLayer(marker); - }); - markers._isClusterLayer = true; - map.addLayer(markers); - return () => { - map.removeLayer(markers); - }; + markerRef.current.setIcon(icon); + markerRef.current.getPopup()?.setContent(renderToString()); } - }, [map, nodes, config?.clustering, config?.showNodeNames]); + }, [node, showNodeNames]); + return null; } +// Clustered markers component +function ClusteredMarkersGroup({ nodes, showNodeNames }: { nodes: NodePosition[]; showNodeNames: boolean }) { + const map = useMap(); + const clusterGroupRef = useRef(null); + + useEffect(() => { + if (!map) return; + + const iconCreateFunction = (cluster: any) => { + const children = cluster.getAllChildMarkers(); + return L.divIcon({ + html: renderToString({children}), + className: 'custom-cluster-icon', + iconSize: [40, 40], + iconAnchor: [20, 20], + }); + }; + + const markers = (L as any).markerClusterGroup({ + iconCreateFunction, + maxClusterRadius: 40, + }); + + nodes.forEach((node: NodePosition) => { + const icon = L.divIcon({ + className: 'custom-node-marker-container', + iconSize: [16, 32], + iconAnchor: [8, 8], + html: renderToString(), + }); + const marker = L.marker([node.latitude, node.longitude], { icon }); + (marker as any).options.nodeData = node; + marker.bindPopup(renderToString()); + markers.addLayer(marker); + }); + + markers._isClusterLayer = true; + map.addLayer(markers); + clusterGroupRef.current = markers; + + return () => { + if (clusterGroupRef.current && map.hasLayer(clusterGroupRef.current)) { + map.removeLayer(clusterGroupRef.current); + } + }; + }, [map, nodes, showNodeNames]); + + return null; +} + +function ClusteredMarkers({ nodes }: ClusteredMarkersProps) { + const configResult = useConfig(); + const config = configResult?.config; + const showNodeNames = config?.showNodeNames !== false; + + if (config?.clustering === false) { + // Render individual marker components + return ( + <> + {nodes.map((node) => ( + + ))} + + ); + } else { + // Render clustered markers + return ( + + ); + } +} + export default function MapView() { const [nodePositions, setNodePositions] = useState([]); const [bounds, setBounds] = useState<[[number, number], [number, number]] | null>(null); @@ -115,7 +176,7 @@ export default function MapView() { const tileLayerOptions: Record = { openstreetmap: { url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", - attribution: 'Tiles © OpenStreetMap | Data from Meshtastic', + attribution: 'Tiles © OpenStreetMap', maxZoom: 22, }, opentopomap: { @@ -125,7 +186,7 @@ export default function MapView() { }, esri: { url: "https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}", - attribution: 'Tiles © Esri | Data from Meshtastic', + attribution: 'Tiles © Esri', maxZoom: 21, }, }; @@ -248,6 +309,7 @@ export default function MapView() { [b.getSouthWest().lat, b.getSouthWest().lng], [b.getNorthEast().lat, b.getNorthEast().lng], ]); + map.attributionControl.setPrefix('map.w0z.is') } }, [map]); return null; @@ -305,7 +367,7 @@ export default function MapView() { opacity={0.7} /> )} - +
); diff --git a/src/lib/meshcore.ts b/src/lib/meshcore.ts index aa79ecb..b0da02e 100644 --- a/src/lib/meshcore.ts +++ b/src/lib/meshcore.ts @@ -191,4 +191,10 @@ export async function decryptMeshcoreGroupMessage({ }); } return null; +} + +export function formatPublicKey(pubKey: string): string { + // Take the first 8 characters, add ellipsis, and then the last 8 characters + const formattedKey = `<${pubKey.slice(0, 8)}...${pubKey.slice(-8)}>`; + return formattedKey; } \ No newline at end of file