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
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();
}

View file

@ -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<typeof setTimeout>;