diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml
index e995147e..a9df34eb 100644
--- a/.github/workflows/deploy.yml
+++ b/.github/workflows/deploy.yml
@@ -90,6 +90,7 @@ jobs:
node test-channel-qr-wiring.js
node test-channel-modal-ux.js
node test-channel-issue-1087.js
+ node test-channel-issue-1101.js
node test-pull-to-reconnect-1091.js
node test-channel-fluid-layout.js
diff --git a/public/channel-qr.js b/public/channel-qr.js
index 671f176c..b4da84be 100644
--- a/public/channel-qr.js
+++ b/public/channel-qr.js
@@ -75,9 +75,11 @@
* every Generate click fall through to "[QR library not loaded]".
* (Issue #1087 bug 1.)
*/
- function generate(name, secretHex, target) {
+ function generate(name, secretHex, target, opts) {
if (!_hasDom() || !target) return;
target.innerHTML = '';
+ opts = opts || {};
+ var qrOnly = !!opts.qrOnly;
const url = buildUrl(name, secretHex);
@@ -113,6 +115,12 @@
qrBox.textContent = '[QR library not loaded]';
}
+ // #1101: in qrOnly mode (Share modal), the host renders the hex
+ // key field + Copy button BELOW the QR. Skip the inline URL line
+ // and inline Copy Key button here so the QR box contains JUST the
+ // QR image — no overlap, no redundant affordances.
+ if (qrOnly) return;
+
const urlLine = document.createElement('div');
urlLine.className = 'channel-qr-url';
urlLine.style.cssText = 'font-family:monospace;font-size:11px;word-break:break-all;margin-top:6px;';
diff --git a/public/channels.js b/public/channels.js
index d98cd7cc..e5b7fd7b 100644
--- a/public/channels.js
+++ b/public/channels.js
@@ -759,13 +759,6 @@
-
-
-
-
-
-
-
⚠ Privacy: only share with trusted people. Anyone with this key can read all messages on this channel.
@@ -882,19 +875,19 @@
if (title) title.textContent = 'Share: ' + safeName;
var qrHolder = document.getElementById('chShareQr');
var keyField = document.getElementById('chShareKey');
- var urlField = document.getElementById('chShareUrl');
var fieldsWrap = shareModalEl.querySelectorAll('.ch-share-field-group');
for (var i = 0; i < fieldsWrap.length; i++) fieldsWrap[i].hidden = false;
- var url = 'meshcore://channel/add?name=' + encodeURIComponent(safeName) +
- '&secret=' + keyHex;
if (keyField) keyField.value = keyHex;
- if (urlField) urlField.value = url;
if (qrHolder) {
qrHolder.innerHTML = '';
if (window.ChannelQR && typeof window.ChannelQR.generate === 'function') {
// #1087 Bug 2: pass the user-facing displayName, NOT the
// internal `psk:` channelName lookup key.
- window.ChannelQR.generate(safeName, keyHex, qrHolder);
+ // #1101: qrOnly=true — render JUST the QR image. The Share
+ // modal has its own dedicated hex key field + Copy button
+ // BELOW the QR; an inline URL line + Copy Key button inside
+ // the QR box was redundant and visually overlapping.
+ window.ChannelQR.generate(safeName, keyHex, qrHolder, { qrOnly: true });
}
}
shareModalEl.classList.remove('hidden');
@@ -946,10 +939,10 @@
var copyBtn = e.target.closest && e.target.closest('[data-share-copy]');
if (copyBtn) {
e.preventDefault();
- var which = copyBtn.getAttribute('data-share-copy');
- var src = which === 'url'
- ? document.getElementById('chShareUrl')
- : document.getElementById('chShareKey');
+ // #1101: only the hex key is copyable from the share modal;
+ // the URL field was removed, so the data-share-copy attribute
+ // is informational only — the source is always #chShareKey.
+ var src = document.getElementById('chShareKey');
if (src) {
try { src.select(); } catch (e2) {}
var doneCopy = function () {
diff --git a/test-channel-issue-1087-e2e.js b/test-channel-issue-1087-e2e.js
index 4d7ba759..69601c42 100644
--- a/test-channel-issue-1087-e2e.js
+++ b/test-channel-issue-1087-e2e.js
@@ -160,15 +160,36 @@ function assert(c, m) { if (!c) throw new Error(m || 'assertion failed'); }
assert(/share/i.test(shareTitle),
'Bug 4: share modal title must contain "Share", got: ' + shareTitle);
- // Hex key + URL fields must be present and copyable.
+ // Hex key field must be present and copyable. (#1101: URL field
+ // removed — QR already encodes the URL, a separate Copy URL button
+ // was redundant.)
const hasFields = await page.evaluate(() => {
const m = document.getElementById('chShareModal');
if (!m) return false;
const k = m.querySelector('#chShareKey, [data-share-field="key"]');
const u = m.querySelector('#chShareUrl, [data-share-field="url"]');
- return !!(k && u);
+ return !!k && !u;
});
- assert(hasFields, 'Bug 4: share modal must expose hex key + URL fields');
+ assert(hasFields, 'Bug 4 / #1101: share modal exposes ONLY the hex key field (no URL field)');
+
+ // #1101: the QR box must contain ONLY the QR — no URL text
+ // line, no inline Copy Key button overlapping the image.
+ const qrBoxOnlyHasQr = await page.evaluate(() => {
+ const qr = document.getElementById('chShareQr');
+ if (!qr) return { ok: false, reason: 'no #chShareQr' };
+ const imgs = qr.querySelectorAll('img');
+ const urlLine = qr.querySelector('.channel-qr-url');
+ const copyBtn = qr.querySelector('.channel-qr-copy, button');
+ return {
+ ok: imgs.length === 1 && !urlLine && !copyBtn,
+ imgCount: imgs.length,
+ hasUrlLine: !!urlLine,
+ hasCopyBtn: !!copyBtn,
+ };
+ });
+ assert(qrBoxOnlyHasQr.ok,
+ '#1101: #chShareQr contains ONLY the QR image (got ' +
+ JSON.stringify(qrBoxOnlyHasQr) + ')');
});
console.log('\n=== Results: ' + passed + ' passed, ' + failed + ' failed ===');
diff --git a/test-channel-issue-1087.js b/test-channel-issue-1087.js
index 3e233a2c..ca3ace34 100644
--- a/test-channel-issue-1087.js
+++ b/test-channel-issue-1087.js
@@ -120,8 +120,8 @@ assert(/id="chShareModalTitle"|class="ch-share-modal-title"|>Share[^<]*")');
assert(/id="chShareKey"|data-share-field="key"/.test(shareModalBlock),
'share modal exposes the hex key field with a copy affordance');
-assert(/id="chShareUrl"|data-share-field="url"/.test(shareModalBlock),
- 'share modal exposes the meshcore:// URL field with a copy affordance');
+// #1101: meshcore:// URL field intentionally REMOVED — QR already
+// encodes the URL, separate field/button was redundant.
assert(/trusted|privacy|do not share|only share/i.test(shareModalBlock),
'share modal includes a privacy warning');
diff --git a/test-channel-issue-1101.js b/test-channel-issue-1101.js
new file mode 100644
index 00000000..d6c178ea
--- /dev/null
+++ b/test-channel-issue-1101.js
@@ -0,0 +1,94 @@
+/**
+ * #1101 — Strip Share modal: remove redundant URL copy + duplicated key field.
+ *
+ * Acceptance criteria:
+ * - Share modal contains only: QR (just the QR image, nothing else
+ * in that box), Hex Key field with single Copy button BELOW the QR,
+ * privacy warning, Close ✕ button.
+ * - No "Copy URL" affordance ANYWHERE in the modal.
+ * - No duplicated meshcore:// URL field below the QR.
+ * - The QR box (#chShareQr) must contain ONLY the QR image — no URL
+ * text, no Copy Key button overlapping it.
+ */
+'use strict';
+
+const fs = require('fs');
+const path = require('path');
+
+let passed = 0, failed = 0;
+function assert(cond, msg) {
+ if (cond) { passed++; console.log(' ✓ ' + msg); }
+ else { failed++; console.error(' ✗ ' + msg); }
+}
+
+const channelsSrc = fs.readFileSync(path.join(__dirname, 'public', 'channels.js'), 'utf8');
+const qrSrc = fs.readFileSync(path.join(__dirname, 'public', 'channel-qr.js'), 'utf8');
+
+console.log('\n=== #1101: Share modal markup ===');
+
+// Locate the share modal markup block.
+const shareModalIdx = channelsSrc.indexOf('id="chShareModal"');
+assert(shareModalIdx > 0, 'share modal markup located');
+// Tighten block isolation: scan forward for the share modal's own
+// closing tag (the outer overlay div is indented 6 spaces, so its
+// matching close is the first "\n " we hit after the
+// opener). Falls back to the old ch-main heuristic if that pattern
+// disappears for any reason.
+let shareEnd = channelsSrc.indexOf('\n ', shareModalIdx);
+if (shareEnd < 0) {
+ shareEnd = channelsSrc.indexOf('
0 && shareModalBlock.length < 4000,
+ 'share modal block isolated');
+
+// Hex key field MUST still be present (single source of truth).
+assert(/id="chShareKey"/.test(shareModalBlock),
+ 'share modal still exposes the hex key field with a Copy button');
+
+// meshcore:// URL field MUST be removed.
+assert(!/id="chShareUrl"/.test(shareModalBlock),
+ 'share modal does NOT render a #chShareUrl input field');
+assert(!/data-share-field="url"/.test(shareModalBlock),
+ 'share modal does NOT render any [data-share-field="url"] element');
+assert(!/data-share-copy="url"/.test(shareModalBlock),
+ 'share modal does NOT render any [data-share-copy="url"] button');
+assert(!/meshcore:\/\/ URL/.test(shareModalBlock),
+ 'share modal does NOT show a "meshcore:// URL" label');
+
+// Privacy warning + close button still required.
+assert(/ch-modal-warn/.test(shareModalBlock),
+ 'share modal still includes the privacy warning');
+assert(/id="chShareModalClose"/.test(shareModalBlock),
+ 'share modal still has the ✕ close button');
+
+console.log('\n=== #1101: openShareModal() body ===');
+
+// openShareModal must no longer reference chShareUrl or build URL into a field.
+const openIdx = channelsSrc.indexOf('function openShareModal(');
+assert(openIdx > 0, 'openShareModal located');
+const openEnd = channelsSrc.indexOf('function ', openIdx + 30);
+const openBlock = channelsSrc.substring(openIdx, openEnd);
+assert(!/getElementById\('chShareUrl'\)/.test(openBlock),
+ 'openShareModal does NOT look up #chShareUrl');
+assert(!/urlField\.value\s*=/.test(openBlock),
+ 'openShareModal does NOT assign to urlField.value');
+
+console.log('\n=== #1101: ChannelQR.generate() supports qrOnly ===');
+
+// ChannelQR.generate must accept an opts.qrOnly flag so the Share
+// modal's QR box can render JUST the QR image — no URL line, no
+// inline Copy Key button. (The Share modal has its own dedicated
+// hex key field + Copy button BELOW the QR.)
+assert(/function generate\([^)]*opts[^)]*\)/.test(qrSrc),
+ 'ChannelQR.generate accepts an opts argument');
+assert(/qrOnly/.test(qrSrc),
+ 'ChannelQR.generate honours opts.qrOnly');
+
+// Share modal call site must pass qrOnly:true.
+assert(/ChannelQR\.generate\([^)]*qrOnly[^)]*\)/.test(channelsSrc) ||
+ /ChannelQR\.generate\([\s\S]{0,200}qrOnly\s*:\s*true/.test(channelsSrc),
+ 'openShareModal passes { qrOnly: true } to ChannelQR.generate');
+
+console.log('\n=== Results: ' + passed + ' passed, ' + failed + ' failed ===');
+process.exit(failed > 0 ? 1 : 0);