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