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.
This commit is contained in:
Graham McIntire 2026-04-27 09:46:34 -05:00
parent 5e277ddb9d
commit f005a1a119
No known key found for this signature in database
GPG key ID: F4ABF488E6029E59
2 changed files with 22 additions and 6 deletions

View file

@ -682,6 +682,10 @@ let MapAPRSMap = {
// Add click handler for spiderfied markers // Add click handler for spiderfied markers
self.oms.addListener("click", function (marker: Marker) { self.oms.addListener("click", function (marker: Marker) {
const aprsMarker = marker as APRSMarker;
if (aprsMarker._markerId) {
self.userOpenedPopupMarkerId = aprsMarker._markerId;
}
if (marker.openPopup) marker.openPopup(); if (marker.openPopup) marker.openPopup();
}); });
@ -760,6 +764,10 @@ let MapAPRSMap = {
}); });
if (nearestMarker) { if (nearestMarker) {
const aprsMarker = nearestMarker as APRSMarker;
if (aprsMarker._markerId) {
self.userOpenedPopupMarkerId = aprsMarker._markerId;
}
nearestMarker.openPopup(); nearestMarker.openPopup();
} }
}; };
@ -1263,8 +1271,10 @@ let MapAPRSMap = {
// Handle highlighting the latest packet (open its popup) // Handle highlighting the latest packet (open its popup)
self.currentPopupMarkerId = null; self.currentPopupMarkerId = null;
self.userOpenedPopupMarkerId = null;
self.handleEvent("highlight_packet", (data: { id: string }) => { self.handleEvent("highlight_packet", (data: { id: string }) => {
if (!data.id || !self.markers || !self.markerStates) return; if (!data.id || !self.markers || !self.markerStates) return;
if (self.userOpenedPopupMarkerId) return;
// Close previous popup if open // Close previous popup if open
if ( if (
@ -1843,8 +1853,8 @@ let MapAPRSMap = {
if (!positionChanged && !dataChanged) { if (!positionChanged && !dataChanged) {
// No changes needed, skip update // No changes needed, skip update
// But if openPopup is requested, open it // But if openPopup is requested and no user popup is locked, open it
if (data.openPopup && existingMarker.openPopup) { if (data.openPopup && existingMarker.openPopup && !self.userOpenedPopupMarkerId) {
existingMarker.openPopup(); existingMarker.openPopup();
} }
return; return;
@ -1898,6 +1908,7 @@ let MapAPRSMap = {
const marker = L.marker([lat, lng], { const marker = L.marker([lat, lng], {
icon: self.createMarkerIcon(data), icon: self.createMarkerIcon(data),
}); });
(marker as APRSMarker)._markerId = data.id;
// Add popup if content provided // Add popup if content provided
if (data.popup) { if (data.popup) {
@ -1905,7 +1916,9 @@ let MapAPRSMap = {
// Handle popup close events - check if hook is still connected // Handle popup close events - check if hook is still connected
marker.on("popupclose", () => { 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 ( if (
!self.isDestroyed && !self.isDestroyed &&
self.pushEvent && self.pushEvent &&
@ -1914,7 +1927,6 @@ let MapAPRSMap = {
try { try {
self.pushEvent("popup_closed", {}); self.pushEvent("popup_closed", {});
} catch (e) { } catch (e) {
// Silently ignore errors if context is lost
console.debug("Unable to send popup_closed event:", e); 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 // The OMS will handle the click and open the popup after spiderfying
if (self.oms && marker._oms && marker._omsData) { if (self.oms && marker._oms && marker._omsData) {
// Marker is currently spiderfied, allow normal popup // Marker is currently spiderfied, allow normal popup
self.userOpenedPopupMarkerId = data.id;
if (marker.openPopup) marker.openPopup(); if (marker.openPopup) marker.openPopup();
} else if (!self.oms || !marker._oms) { } else if (!self.oms || !marker._oms) {
// Normal marker not managed by OMS // Normal marker not managed by OMS
self.userOpenedPopupMarkerId = data.id;
if (marker.openPopup) marker.openPopup(); if (marker.openPopup) marker.openPopup();
} }
// If marker._oms is true but no _omsData, let OMS handle it (will spiderfy) // If marker._oms is true but no _omsData, let OMS handle it (will spiderfy)
@ -2059,8 +2073,8 @@ let MapAPRSMap = {
); );
} }
// Open popup if requested // Open popup if requested and no user popup is locked
if (data.openPopup && marker.openPopup) { if (data.openPopup && marker.openPopup && !self.userOpenedPopupMarkerId) {
marker.openPopup(); marker.openPopup();
} }

View file

@ -23,6 +23,8 @@ export interface LiveViewHookContext {
maxInitializationAttempts?: number; maxInitializationAttempts?: number;
lastZoom?: number; lastZoom?: number;
currentPopupMarkerId?: string | null; currentPopupMarkerId?: string | null;
userOpenedPopupMarkerId?: string | null;
markerZIndexCounter?: number;
oms?: OverlappingMarkerSpiderfier; oms?: OverlappingMarkerSpiderfier;
programmaticMoveId?: string; programmaticMoveId?: string;
programmaticMoveTimeout?: ReturnType<typeof setTimeout>; programmaticMoveTimeout?: ReturnType<typeof setTimeout>;