From f005a1a119cf9a4857734946e488a25ae93bee35 Mon Sep 17 00:00:00 2001 From: Graham McIntire Date: Mon, 27 Apr 2026 09:46:34 -0500 Subject: [PATCH] fix: prevent incoming packets from closing user-opened map popup Add a client-side userOpenedPopupMarkerId lock that's set instantly when a user manually opens a popup (via click, OMS spiderfy click, or long-press) and cleared when the popup closes. All programmatic openPopup paths (highlight_packet, addMarker openPopup flag) now respect the lock. Closes the network round-trip race where new packets arriving between the user's click and the server's marker_clicked ack could steal the popup. --- assets/js/map.ts | 26 ++++++++++++++++++++------ assets/js/types/map.d.ts | 2 ++ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/assets/js/map.ts b/assets/js/map.ts index c7e9d28..e666624 100644 --- a/assets/js/map.ts +++ b/assets/js/map.ts @@ -682,6 +682,10 @@ let MapAPRSMap = { // Add click handler for spiderfied markers self.oms.addListener("click", function (marker: Marker) { + const aprsMarker = marker as APRSMarker; + if (aprsMarker._markerId) { + self.userOpenedPopupMarkerId = aprsMarker._markerId; + } if (marker.openPopup) marker.openPopup(); }); @@ -760,6 +764,10 @@ let MapAPRSMap = { }); if (nearestMarker) { + const aprsMarker = nearestMarker as APRSMarker; + if (aprsMarker._markerId) { + self.userOpenedPopupMarkerId = aprsMarker._markerId; + } nearestMarker.openPopup(); } }; @@ -1263,8 +1271,10 @@ let MapAPRSMap = { // Handle highlighting the latest packet (open its popup) self.currentPopupMarkerId = null; + self.userOpenedPopupMarkerId = null; self.handleEvent("highlight_packet", (data: { id: string }) => { if (!data.id || !self.markers || !self.markerStates) return; + if (self.userOpenedPopupMarkerId) return; // Close previous popup if open if ( @@ -1843,8 +1853,8 @@ let MapAPRSMap = { if (!positionChanged && !dataChanged) { // No changes needed, skip update - // But if openPopup is requested, open it - if (data.openPopup && existingMarker.openPopup) { + // But if openPopup is requested and no user popup is locked, open it + if (data.openPopup && existingMarker.openPopup && !self.userOpenedPopupMarkerId) { existingMarker.openPopup(); } return; @@ -1898,6 +1908,7 @@ let MapAPRSMap = { const marker = L.marker([lat, lng], { icon: self.createMarkerIcon(data), }); + (marker as APRSMarker)._markerId = data.id; // Add popup if content provided if (data.popup) { @@ -1905,7 +1916,9 @@ let MapAPRSMap = { // Handle popup close events - check if hook is still connected marker.on("popupclose", () => { - // Only send event if not destroyed and pushEvent is still the original function + if (self.userOpenedPopupMarkerId === data.id) { + self.userOpenedPopupMarkerId = null; + } if ( !self.isDestroyed && self.pushEvent && @@ -1914,7 +1927,6 @@ let MapAPRSMap = { try { self.pushEvent("popup_closed", {}); } catch (e) { - // Silently ignore errors if context is lost console.debug("Unable to send popup_closed event:", e); } } @@ -1927,9 +1939,11 @@ let MapAPRSMap = { // The OMS will handle the click and open the popup after spiderfying if (self.oms && marker._oms && marker._omsData) { // Marker is currently spiderfied, allow normal popup + self.userOpenedPopupMarkerId = data.id; if (marker.openPopup) marker.openPopup(); } else if (!self.oms || !marker._oms) { // Normal marker not managed by OMS + self.userOpenedPopupMarkerId = data.id; if (marker.openPopup) marker.openPopup(); } // If marker._oms is true but no _omsData, let OMS handle it (will spiderfy) @@ -2059,8 +2073,8 @@ let MapAPRSMap = { ); } - // Open popup if requested - if (data.openPopup && marker.openPopup) { + // Open popup if requested and no user popup is locked + if (data.openPopup && marker.openPopup && !self.userOpenedPopupMarkerId) { marker.openPopup(); } diff --git a/assets/js/types/map.d.ts b/assets/js/types/map.d.ts index 6178f9e..6747b99 100644 --- a/assets/js/types/map.d.ts +++ b/assets/js/types/map.d.ts @@ -23,6 +23,8 @@ export interface LiveViewHookContext { maxInitializationAttempts?: number; lastZoom?: number; currentPopupMarkerId?: string | null; + userOpenedPopupMarkerId?: string | null; + markerZIndexCounter?: number; oms?: OverlappingMarkerSpiderfier; programmaticMoveId?: string; programmaticMoveTimeout?: ReturnType;