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