mirror of
https://github.com/ajvpot/meshexplorer.git
synced 2026-08-07 09:02:44 +02:00
Merge branch 'ajvpot:main' into main
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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 (
|
||||
<div className="custom-node-marker-container">
|
||||
{showNodeNames && node.short_name && (
|
||||
<div className="custom-node-label">{node.short_name}</div>
|
||||
<div className="custom-node-label">
|
||||
{node.type === "meshcore" ? getNameIconLabel(node.name || node.short_name) : node.short_name}
|
||||
</div>
|
||||
)}
|
||||
<div className={getMarkerClass()}></div>
|
||||
</div>
|
||||
@@ -144,9 +148,9 @@ export function ClusterMarker({ children }: ClusterMarkerProps) {
|
||||
export function PopupContent({ node }: PopupContentProps) {
|
||||
return (
|
||||
<div>
|
||||
<div><b>ID:</b> {node.node_id}</div>
|
||||
<div><b>ID:</b> {node.type === "meshcore" ? formatPublicKey(node.node_id) : node.node_id}</div>
|
||||
<div><b>Full Name:</b> {node.name ?? "-"}</div>
|
||||
<div><b>Short Name:</b> {node.short_name ?? "-"}</div>
|
||||
<div><b>Short Name:</b> {node.type === "meshcore" && node.short_name ? getNameIconLabel(node.name || node.short_name) : (node.short_name ?? "-")}</div>
|
||||
<div><b>Type:</b> {node.type ?? "-"}</div>
|
||||
<div><b>Lat:</b> {node.latitude}</div>
|
||||
<div><b>Lng:</b> {node.longitude}</div>
|
||||
|
||||
+126
-64
@@ -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<L.Marker | null>(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(<NodeMarker node={node} showNodeNames={showNodeNames} />),
|
||||
});
|
||||
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(<NodeMarker node={node} showNodeNames={config?.showNodeNames !== false} />),
|
||||
});
|
||||
const marker = L.marker([node.latitude, node.longitude], { icon });
|
||||
(marker as any).options.nodeData = node;
|
||||
marker.bindPopup(renderToString(<PopupContent node={node} />));
|
||||
marker.addTo(map);
|
||||
markerLayers.push(marker);
|
||||
|
||||
const marker = L.marker([node.latitude, node.longitude], { icon });
|
||||
(marker as any).options.nodeData = node;
|
||||
marker.bindPopup(renderToString(<PopupContent node={node} />));
|
||||
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(<NodeMarker node={node} showNodeNames={showNodeNames} />),
|
||||
});
|
||||
// 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(<ClusterMarker>{children}</ClusterMarker>),
|
||||
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(<NodeMarker node={node} showNodeNames={config?.showNodeNames !== false} />),
|
||||
});
|
||||
const marker = L.marker([node.latitude, node.longitude], { icon });
|
||||
(marker as any).options.nodeData = node;
|
||||
marker.bindPopup(renderToString(<PopupContent node={node} />));
|
||||
markers.addLayer(marker);
|
||||
});
|
||||
markers._isClusterLayer = true;
|
||||
map.addLayer(markers);
|
||||
return () => {
|
||||
map.removeLayer(markers);
|
||||
};
|
||||
markerRef.current.setIcon(icon);
|
||||
markerRef.current.getPopup()?.setContent(renderToString(<PopupContent node={node} />));
|
||||
}
|
||||
}, [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<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const iconCreateFunction = (cluster: any) => {
|
||||
const children = cluster.getAllChildMarkers();
|
||||
return L.divIcon({
|
||||
html: renderToString(<ClusterMarker>{children}</ClusterMarker>),
|
||||
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(<NodeMarker node={node} showNodeNames={showNodeNames} />),
|
||||
});
|
||||
const marker = L.marker([node.latitude, node.longitude], { icon });
|
||||
(marker as any).options.nodeData = node;
|
||||
marker.bindPopup(renderToString(<PopupContent node={node} />));
|
||||
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) => (
|
||||
<IndividualMarker
|
||||
key={node.node_id}
|
||||
node={node}
|
||||
showNodeNames={showNodeNames}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
// Render clustered markers
|
||||
return (
|
||||
<ClusteredMarkersGroup
|
||||
nodes={nodes}
|
||||
showNodeNames={showNodeNames}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default function MapView() {
|
||||
const [nodePositions, setNodePositions] = useState<NodePosition[]>([]);
|
||||
const [bounds, setBounds] = useState<[[number, number], [number, number]] | null>(null);
|
||||
@@ -115,7 +176,7 @@ export default function MapView() {
|
||||
const tileLayerOptions: Record<TileLayerKey, { url: string; attribution: string; maxZoom: number; subdomains?: string[] }> = {
|
||||
openstreetmap: {
|
||||
url: "https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png",
|
||||
attribution: 'Tiles © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
|
||||
attribution: 'Tiles © <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a>',
|
||||
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 © <a href="https://developers.arcgis.com/documentation/mapping-apis-and-services/deployment/basemap-attribution/">Esri</a> | Data from <a target="_blank" href="https://meshtastic.org/docs/software/integrations/mqtt/">Meshtastic</a>',
|
||||
attribution: 'Tiles © <a href="https://developers.arcgis.com/documentation/mapping-apis-and-services/deployment/basemap-attribution/">Esri</a>',
|
||||
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}
|
||||
/>
|
||||
)}
|
||||
<ClusteredMarkers nodes={nodePositions} />
|
||||
<ClusteredMarkers key={`clustering-${config?.clustering}-${config?.showNodeNames}`} nodes={nodePositions} />
|
||||
</MapContainer>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user