From 08ba91b9ba83a0194fa0ad36a04d3744f345052a Mon Sep 17 00:00:00 2001 From: MarekWo Date: Mon, 23 Mar 2026 09:38:29 +0100 Subject: [PATCH] fix(paths): allow non-adjacent duplicate hops for 1-byte paths For 1B hash size, duplicate repeater IDs are valid as long as they don't appear consecutively (e.g. AA->BB->CC->AA->EE works fine). For 2B/3B, duplicates remain fully blocked. Applied to all three input methods: list picker, map picker, and manual entry. Co-Authored-By: Claude Opus 4.6 --- app/static/js/dm.js | 47 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 37 insertions(+), 10 deletions(-) diff --git a/app/static/js/dm.js b/app/static/js/dm.js index 2e3015d..61930af 100644 --- a/app/static/js/dm.js +++ b/app/static/js/dm.js @@ -1855,10 +1855,20 @@ function setupPathFormHandlers(pubkey) { for (let i = 0; i < pathHex.length; i += chunk) { hops.push(pathHex.substring(i, i + chunk).toLowerCase()); } - const dupes = hops.filter((h, i) => hops.indexOf(h) !== i); - if (dupes.length > 0) { - showNotification(`Duplicate hop(s): ${[...new Set(dupes)].map(d => d.toUpperCase()).join(', ')}`, 'danger'); - return; + if (hashSize === 1) { + // 1B: only block adjacent duplicates + const adjDupes = hops.filter((h, i) => i > 0 && hops[i - 1] === h); + if (adjDupes.length > 0) { + showNotification(`Adjacent duplicate hop(s): ${[...new Set(adjDupes)].map(d => d.toUpperCase()).join(', ')}`, 'danger'); + return; + } + } else { + // 2B/3B: block any duplicate + const dupes = hops.filter((h, i) => hops.indexOf(h) !== i); + if (dupes.length > 0) { + showNotification(`Duplicate hop(s): ${[...new Set(dupes)].map(d => d.toUpperCase()).join(', ')}`, 'danger'); + return; + } } try { @@ -2007,9 +2017,19 @@ function renderRepeaterList(listEl, repeaters, pubkey) { item.addEventListener('click', () => { // Check for duplicate hop const existingHops = getCurrentPathHops(hashSize); - if (existingHops.includes(prefix.toLowerCase())) { - showNotification(`${prefix} is already in the path`, 'warning'); - return; + const prefixLc = prefix.toLowerCase(); + if (hashSize === 1) { + // 1B: only block if same as last hop (adjacent duplicate) + if (existingHops.length > 0 && existingHops[existingHops.length - 1] === prefixLc) { + showNotification(`${prefix} cannot be adjacent to itself`, 'warning'); + return; + } + } else { + // 2B/3B: block any duplicate + if (existingHops.includes(prefixLc)) { + showNotification(`${prefix} is already in the path`, 'warning'); + return; + } } // Append hop to path hex input const current = hexInput.value.replace(/[,\s→]/g, '').trim(); @@ -2136,9 +2156,16 @@ function openRepeaterMapPicker() { const prefix = _rptMapSelectedRepeater.public_key.substring(0, hashSize * 2).toLowerCase(); // Check for duplicate hop const existingHops = getCurrentPathHops(hashSize); - if (existingHops.includes(prefix)) { - showNotification(`${prefix.toUpperCase()} is already in the path`, 'warning'); - return; + if (hashSize === 1) { + if (existingHops.length > 0 && existingHops[existingHops.length - 1] === prefix) { + showNotification(`${prefix.toUpperCase()} cannot be adjacent to itself`, 'warning'); + return; + } + } else { + if (existingHops.includes(prefix)) { + showNotification(`${prefix.toUpperCase()} is already in the path`, 'warning'); + return; + } } const hexInput = document.getElementById('dmPathHexInput'); if (hexInput) {