From 2c38f04fa193a80910c829ad2bed82fc3e3776df Mon Sep 17 00:00:00 2001 From: jolavillette Date: Sun, 16 Aug 2026 09:45:02 +0200 Subject: [PATCH] webui v141: stop the endless getIdDetails retry, and one copy path fetchIdDetails() restored `undefined` after its five attempts, which is the very value that makes it fire a request -- and two of its callers sit inside a view (people_sidebar's filter and map). Every redraw therefore restarted the whole six request chain, forever, for any identity the core never resolves. Mark the give-up state with `null` instead, like the loading state: attempted, not to be asked again. The copy icon still ran the old inline execCommand path, so copyId() was only reachable through shareId()'s fallback and its clipboard call could reject with no feedback at all. The icon now calls copyId(), the Clipboard API failure falls back to execCommand, and the popup reports a refused copy instead of claiming success. Same role/tabindex/keyboard treatment as the share icon next to it. --- webui-src/app/home.js | 48 ++++++++++++++++++++++------ webui-src/app/main.js | 4 +-- webui-src/app/people/people_state.js | 7 +++- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/webui-src/app/home.js b/webui-src/app/home.js index 840ce8f..56db728 100644 --- a/webui-src/app/home.js +++ b/webui-src/app/home.js @@ -79,15 +79,39 @@ const retroshareId = () => { el.style.height = el.scrollHeight + 'px'; } + function copyIdFallback() { + const field = document.getElementById('retroId'); + if (!field) return false; + field.select(); + // Deprecated, but the Clipboard API is only exposed in a secure context + // and the web UI is normally served over plain http on the LAN. + return document.execCommand('copy'); + } + async function copyId(value) { + let copied; if (navigator.clipboard && navigator.clipboard.writeText) { - await navigator.clipboard.writeText(value); + try { + await navigator.clipboard.writeText(value); + copied = true; + } catch (_) { + // Denied permission or an unfocused document: fall back rather than + // leaving the promise rejected and no feedback at all. + copied = copyIdFallback(); + } } else { - const field = document.getElementById('retroId'); - field.select(); - document.execCommand('copy'); + copied = copyIdFallback(); } - widget.popupMessage(m(ConfirmCopied), 'copy-confirmation-modal'); + widget.popupMessage( + copied + ? m(ConfirmCopied) + : [ + m('h3', 'Copy failed'), + m('hr'), + m('p', 'Your browser refused the copy. Select the ID above and copy it by hand.'), + ], + 'copy-confirmation-modal' + ); } async function shareId(value) { @@ -124,10 +148,16 @@ const retroshareId = () => { v.attrs.ownCert ), m('i.fas.fa-copy', { - onclick: () => { - document.getElementById('retroId').select(); - document.execCommand('copy'); - widget.popupMessage(m(ConfirmCopied), 'copy-confirmation-modal'); + role: 'button', + tabindex: 0, + title: 'Copy RetroShare ID', + 'aria-label': 'Copy RetroShare ID', + onclick: () => copyId(v.attrs.ownCert), + onkeydown: (event) => { + if (event.key === 'Enter' || event.key === ' ') { + event.preventDefault(); + copyId(v.attrs.ownCert); + } }, }), m('i.fas.fa-share-alt', { diff --git a/webui-src/app/main.js b/webui-src/app/main.js index e6ad91d..0720d3d 100644 --- a/webui-src/app/main.js +++ b/webui-src/app/main.js @@ -114,7 +114,7 @@ const navbar = () => { ? 'Connected to RetroShare Core' : 'Connection Lost', }), - m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v140'), + m('span.webui-version', { style: { fontSize: '0.7em' } }, 'v141'), m('i.fas.fa-sync-alt.refresh-icon', { style: { cursor: 'pointer', fontSize: '0.8em' }, onclick: () => window.location.reload(true), @@ -236,7 +236,7 @@ const MobileStatus = () => { m('small', statusbar.formatBytes(state.totalOut)), ]), ]), - m('.mobile-status-sheet__version', 'WebUI v140'), + m('.mobile-status-sheet__version', 'WebUI v141'), ])), ]; }, diff --git a/webui-src/app/people/people_state.js b/webui-src/app/people/people_state.js index 03b3c60..bb26a56 100644 --- a/webui-src/app/people/people_state.js +++ b/webui-src/app/people/people_state.js @@ -65,7 +65,12 @@ function fetchIdDetails(gxsId, attempt = 0) { fetchIdDetails(gxsId, attempt + 1); }, 250 * (attempt + 1)); } else { - State.gxsIdToDetailsMap[gxsId] = undefined; + // Give up on this id, but do NOT restore `undefined`: that is the + // value which makes this function fire a request, and two of the + // callers sit inside a view (people_sidebar). Every redraw would then + // start the whole six request chain again, forever, for any id the + // core never resolves. `null` keeps the entry marked as attempted. + State.gxsIdToDetailsMap[gxsId] = null; } }); }