Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,7 @@ jobs:
node test-issue-1509-nav-active-bg.js
node test-issue-1509-detect-preset.js
node test-live.js
node test-issue-125-live-toggles-wiring.js
node test-coverage-gate.js
node test-node-reach-coverage.js
node test-reach-rank.js
Expand Down Expand Up @@ -502,6 +503,7 @@ jobs:
BASE_URL=http://localhost:13581 node test-issue-1128-multi-viewport-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1136-live-region-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-live-multibyte-only-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-125-live-toggles-early-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1150-404-state-e2e.js 2>&1 | tee -a e2e-output.txt
BASE_URL=http://localhost:13581 node test-issue-1146-path-link-contrast-e2e.js 2>&1 | tee -a e2e-output.txt
CHROMIUM_REQUIRE=1 node test-issue-1705-subpath-contrast-e2e.js 2>&1 | tee -a e2e-output.txt
Expand Down
201 changes: 106 additions & 95 deletions public/live.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@
let _lastAnimFrame = 0;
let nodeActivity = {};
let recentPaths = [];
// Heat is a mirror like the toggles below (#125): wireLiveControls()
// restores from it and its change handler writes it back.
let heatEnabled = localStorage.getItem('meshcore-live-heatmap') !== 'false';
let showGhostHops = localStorage.getItem('live-ghost-hops') !== 'false';
let realisticPropagation = localStorage.getItem('live-realistic-propagation') === 'true';
let showOnlyFavorites = localStorage.getItem('live-favorites-only') === 'true';
Expand Down Expand Up @@ -1123,6 +1126,102 @@
startReplay();
}

/**
* Restore and wire the persisted Live view toggles (#125).
*
* MUST run synchronously right after init() writes the markup, before init
* awaits anything: until it runs the checkboxes are painted and clickable
* but show their default state, and a click is neither saved nor kept.
* Restoring state and attaching listeners never sits behind an await; the
* visible EFFECT of a setting that needs the map, markers or heat layer is
* applied again by applyLiveControlEffects() once init has built them.
*
* Idempotent per element (data-live-wired). init() writes new markup on
* every mount, so each mounted checkbox has exactly one listener.
*
* Not covered, deliberately: #liveAudioToggle (MeshAudio.restore() and its
* slider panel; audio needs a user gesture anyway) and the geo-filter and
* region controls, which stay hidden or inert until their own fetches.
*/
function wireLiveControls() {
const TOGGLES = [
{ id: 'liveHeatToggle', restore: () => heatEnabled, onChange: (v) => {
heatEnabled = v;
localStorage.setItem('meshcore-live-heatmap', heatEnabled);
if (v) showHeatMap(); else hideHeatMap();
} },
{ id: 'liveGhostToggle', restore: () => showGhostHops, onChange: (v) => {
showGhostHops = v;
localStorage.setItem('live-ghost-hops', showGhostHops);
} },
{ id: 'liveRealisticToggle', restore: () => realisticPropagation, onChange: (v) => {
realisticPropagation = v;
localStorage.setItem('live-realistic-propagation', realisticPropagation);
} },
{ id: 'liveColorHashToggle', restore: () => colorByHash, onChange: (v) => {
colorByHash = v;
localStorage.setItem('meshcore-color-packets-by-hash', colorByHash);
window.dispatchEvent(new Event('storage'));
} },
{ id: 'liveFavoritesToggle', restore: () => showOnlyFavorites, onChange: (v) => {
showOnlyFavorites = v;
localStorage.setItem('live-favorites-only', showOnlyFavorites);
applyFavoritesFilter();
} },
{ id: 'liveForeignToggle', restore: () => highlightForeign, onChange: (v) => {
highlightForeign = v;
localStorage.setItem('live-highlight-foreign', highlightForeign);
} },
{ id: 'liveMultibyteToggle', restore: () => multibyteOnly, onChange: (v) => {
multibyteOnly = v;
localStorage.setItem('live-multibyte-only', multibyteOnly);
rebuildFeedList();
} },
{ id: 'liveMatrixToggle', restore: () => matrixMode, onChange: (v) => {
matrixMode = v;
localStorage.setItem('live-matrix-mode', matrixMode);
applyMatrixTheme(matrixMode);
syncHeatToggleToMatrix(matrixMode);
} },
{ id: 'liveMatrixRainToggle', restore: () => matrixRain, onChange: (v) => {
matrixRain = v;
localStorage.setItem('live-matrix-rain', matrixRain);
if (matrixRain) startMatrixRain(); else stopMatrixRain();
} },
];
for (const t of TOGGLES) {
const el = document.getElementById(t.id);
if (!el) { console.warn('[live] control not found: ' + t.id); continue; }
if (el.dataset.liveWired === '1') continue;
el.dataset.liveWired = '1';
el.checked = t.restore();
el.addEventListener('change', (e) => t.onChange(e.target.checked));
}
// The Heat/Matrix interlock holds from the first paint.
syncHeatToggleToMatrix(matrixMode);
}

// Matrix mode owns the heat map: while it is on, the heat layer is hidden
// and its toggle unchecked and disabled. Safe before the map exists.
function syncHeatToggleToMatrix(on) {
const ht = document.getElementById('liveHeatToggle');
if (on) {
hideHeatMap();
if (ht) { ht.checked = false; ht.disabled = true; }
} else if (ht) {
ht.disabled = false; // recover from stale state
}
}

// Effects of the restored toggles that need what init() builds behind its
// awaits: the map, the node markers and the heat layer.
function applyLiveControlEffects() {
if (heatEnabled && !matrixMode) showHeatMap(); else hideHeatMap();
applyMatrixTheme(matrixMode);
syncHeatToggleToMatrix(matrixMode);
if (matrixRain) startMatrixRain();
}

async function init(app) {
app.innerHTML = `
<div class="live-page">
Expand Down Expand Up @@ -1276,18 +1375,9 @@
</div>
</div>`;

// "Multibyte only" is restored and wired before the first await. The
// controls are visible and clickable as soon as they are rendered, and the
// feed already filters on the saved multibyteOnly. Wired after the awaits
// below, the box showed OFF while init ran even when saved ON, and a click
// made then was not saved and was reset when init caught up.
const multibyteToggle = document.getElementById('liveMultibyteToggle');
multibyteToggle.checked = multibyteOnly;
multibyteToggle.addEventListener('change', (e) => {
multibyteOnly = e.target.checked;
localStorage.setItem('live-multibyte-only', multibyteOnly);
rebuildFeedList();
});
// The persisted view toggles are restored and wired before the first
// await (#88, #125): see wireLiveControls().
wireLiveControls();

// Fetch configurable map defaults (#115)
let mapCenter = [37.45, -122.0];
Expand Down Expand Up @@ -1581,7 +1671,7 @@
AreaFilter.init(document.getElementById('liveAreaFilter'));
AreaFilter.onChange(function () { loadNodes(); });
await loadNodes();
showHeatMap();
applyLiveControlEffects();
connectWS();
initResizeHandler();
initVCRHeightTracker();
Expand Down Expand Up @@ -1610,52 +1700,6 @@

map.on('zoomend', rescaleMarkers);

// Heat map toggle — persist in localStorage
const liveHeatEl = document.getElementById('liveHeatToggle');
if (localStorage.getItem('meshcore-live-heatmap') === 'false') { liveHeatEl.checked = false; hideHeatMap(); }
else if (localStorage.getItem('meshcore-live-heatmap') === 'true') { liveHeatEl.checked = true; }
liveHeatEl.addEventListener('change', (e) => {
localStorage.setItem('meshcore-live-heatmap', e.target.checked);
if (e.target.checked) showHeatMap(); else hideHeatMap();
});

const ghostToggle = document.getElementById('liveGhostToggle');
ghostToggle.checked = showGhostHops;
ghostToggle.addEventListener('change', (e) => {
showGhostHops = e.target.checked;
localStorage.setItem('live-ghost-hops', showGhostHops);
});

const realisticToggle = document.getElementById('liveRealisticToggle');
realisticToggle.checked = realisticPropagation;
realisticToggle.addEventListener('change', (e) => {
realisticPropagation = e.target.checked;
localStorage.setItem('live-realistic-propagation', realisticPropagation);
});

const colorHashToggle = document.getElementById('liveColorHashToggle');
colorHashToggle.checked = colorByHash;
colorHashToggle.addEventListener('change', (e) => {
colorByHash = e.target.checked;
localStorage.setItem('meshcore-color-packets-by-hash', colorByHash);
window.dispatchEvent(new Event('storage'));
});

const favoritesToggle = document.getElementById('liveFavoritesToggle');
favoritesToggle.checked = showOnlyFavorites;
favoritesToggle.addEventListener('change', (e) => {
showOnlyFavorites = e.target.checked;
localStorage.setItem('live-favorites-only', showOnlyFavorites);
applyFavoritesFilter();
});

const foreignToggle = document.getElementById('liveForeignToggle');
foreignToggle.checked = highlightForeign;
foreignToggle.addEventListener('change', (e) => {
highlightForeign = e.target.checked;
localStorage.setItem('live-highlight-foreign', highlightForeign);
});

// Region filter (#1045): dropdown of observer IATA regions
(function initLiveRegionFilter() {
var rfEl = document.getElementById('liveRegionFilter');
Expand Down Expand Up @@ -1962,41 +2006,6 @@
refresh();
})();

const matrixToggle = document.getElementById('liveMatrixToggle');
matrixToggle.checked = matrixMode;
matrixToggle.addEventListener('change', (e) => {
matrixMode = e.target.checked;
localStorage.setItem('live-matrix-mode', matrixMode);
applyMatrixTheme(matrixMode);
if (matrixMode) {
hideHeatMap();
const ht = document.getElementById('liveHeatToggle');
if (ht) { ht.checked = false; ht.disabled = true; }
} else {
const ht = document.getElementById('liveHeatToggle');
if (ht) { ht.disabled = false; }
}
});
applyMatrixTheme(matrixMode);
if (matrixMode) {
hideHeatMap();
const ht = document.getElementById('liveHeatToggle');
if (ht) { ht.checked = false; ht.disabled = true; }
} else {
// Ensure heat toggle is enabled if matrix mode is off (recover from stale state)
const ht = document.getElementById('liveHeatToggle');
if (ht) { ht.disabled = false; }
}

const rainToggle = document.getElementById('liveMatrixRainToggle');
rainToggle.checked = matrixRain;
rainToggle.addEventListener('change', (e) => {
matrixRain = e.target.checked;
localStorage.setItem('live-matrix-rain', matrixRain);
if (matrixRain) startMatrixRain(); else stopMatrixRain();
});
if (matrixRain) startMatrixRain();

// Audio toggle
const audioToggle = document.getElementById('liveAudioToggle');
const audioControls = document.getElementById('audioControls');
Expand Down Expand Up @@ -4394,6 +4403,7 @@
}

function showHeatMap() {
if (!map) return; // a Heat click during init: applyLiveControlEffects() builds it
if (heatLayer) { map.removeLayer(heatLayer); heatLayer = null; }
const points = [];
Object.values(nodeData).forEach(n => {
Expand Down Expand Up @@ -4421,7 +4431,8 @@
}

function hideHeatMap() {
if (heatLayer) { map.removeLayer(heatLayer); heatLayer = null; }
if (heatLayer && map) map.removeLayer(heatLayer);
heatLayer = null;
}

/** Extract channel row style from a packet (shared by feed item builders). */
Expand Down
1 change: 1 addition & 0 deletions test-all.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ node test-issue-1883-redirect-history.js
node test-issue-1890-og-url.js
node test-traces.js
node test-live-multibyte-filter.js
node test-issue-125-live-toggles-wiring.js
node test-issue-2052-touch-target-css.js
node test-channel-proposals.js
node test-packet-detail-channel-xss.js
Expand Down
Loading
Loading