Fix link/address copy over plain-HTTP LAN; show group link URL inline
navigator.clipboard only works in a secure context, so copy silently failed when served over a LAN IP on http. Add a robustCopy() with a textarea+execCommand fallback (used by group-link, address and channel-link copy). The group/channel 'Link' button now toggles a visible, selectable URL row beneath the group with a working copy button. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -299,9 +299,23 @@ function onAvatarChange(input) {
|
|||||||
reader.readAsDataURL(file);
|
reader.readAsDataURL(file);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clipboard that also works over plain-HTTP LAN (navigator.clipboard needs a secure context).
|
||||||
|
function robustCopy(text) {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
return navigator.clipboard.writeText(text).catch(() => fallbackCopy(text));
|
||||||
|
}
|
||||||
|
return Promise.resolve(fallbackCopy(text));
|
||||||
|
}
|
||||||
|
function fallbackCopy(text) {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||||||
|
document.body.appendChild(ta); ta.focus(); ta.select();
|
||||||
|
try { document.execCommand('copy'); } catch (e) {}
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
function copyAddr(ev, btn, addr) {
|
function copyAddr(ev, btn, addr) {
|
||||||
ev.stopPropagation();
|
ev.stopPropagation();
|
||||||
navigator.clipboard.writeText(addr).then(() => {
|
robustCopy(addr).then(() => {
|
||||||
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
|
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
|
||||||
setTimeout(() => btn.innerHTML = '<i class="fa-solid fa-copy"></i>', 1500);
|
setTimeout(() => btn.innerHTML = '<i class="fa-solid fa-copy"></i>', 1500);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -198,6 +198,15 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
|
<tr id="link-row-{{ gid }}" style="display:none;">
|
||||||
|
<td colspan="3" style="padding-top:0;">
|
||||||
|
<div class="addr-row" style="margin:0;">
|
||||||
|
<button class="btn btn-ghost copy-btn" title="Copy link"
|
||||||
|
onclick="copyGroupLinkBtn({{ gid }}, this)"><i class="fa-solid fa-copy"></i></button>
|
||||||
|
<a class="addr-link" id="link-url-{{ gid }}" href="#" target="_blank" rel="noopener"></a>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
<!-- Groups -->
|
<!-- Groups -->
|
||||||
@@ -369,8 +378,23 @@ async function sendMsg() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Clipboard that also works over plain-HTTP LAN (navigator.clipboard needs a
|
||||||
|
// secure context). Falls back to a hidden textarea + execCommand.
|
||||||
|
function robustCopy(text) {
|
||||||
|
if (navigator.clipboard && window.isSecureContext) {
|
||||||
|
return navigator.clipboard.writeText(text).catch(() => fallbackCopy(text));
|
||||||
|
}
|
||||||
|
return Promise.resolve(fallbackCopy(text));
|
||||||
|
}
|
||||||
|
function fallbackCopy(text) {
|
||||||
|
const ta = document.createElement('textarea');
|
||||||
|
ta.value = text; ta.style.position = 'fixed'; ta.style.opacity = '0';
|
||||||
|
document.body.appendChild(ta); ta.focus(); ta.select();
|
||||||
|
try { document.execCommand('copy'); } catch (e) {}
|
||||||
|
document.body.removeChild(ta);
|
||||||
|
}
|
||||||
function copyAddr(btn, addr) {
|
function copyAddr(btn, addr) {
|
||||||
navigator.clipboard.writeText(addr).then(() => {
|
robustCopy(addr).then(() => {
|
||||||
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
|
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
|
||||||
setTimeout(() => btn.innerHTML = '<i class="fa-solid fa-copy"></i>', 1500);
|
setTimeout(() => btn.innerHTML = '<i class="fa-solid fa-copy"></i>', 1500);
|
||||||
});
|
});
|
||||||
@@ -504,7 +528,7 @@ async function createGroup() {
|
|||||||
|
|
||||||
function copyChLink() {
|
function copyChLink() {
|
||||||
const val = document.getElementById('ch-link-out').value;
|
const val = document.getElementById('ch-link-out').value;
|
||||||
navigator.clipboard.writeText(val).then(() => {
|
robustCopy(val).then(() => {
|
||||||
document.getElementById('ch-result').textContent = '✓ Copied';
|
document.getElementById('ch-result').textContent = '✓ Copied';
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -572,20 +596,37 @@ async function leaveGroup(groupId, name, btn) {
|
|||||||
else { btn.disabled = false; btn.textContent = 'Leave'; alert('Failed to leave: ' + (data.detail || 'unknown')); }
|
else { btn.disabled = false; btn.textContent = 'Leave'; alert('Failed to leave: ' + (data.detail || 'unknown')); }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function copyGroupLinkBtn(gid, btn) {
|
||||||
|
const url = document.getElementById('link-url-' + gid).textContent;
|
||||||
|
robustCopy(url).then(() => {
|
||||||
|
btn.innerHTML = '<i class="fa-solid fa-check"></i>';
|
||||||
|
setTimeout(() => btn.innerHTML = '<i class="fa-solid fa-copy"></i>', 1500);
|
||||||
|
});
|
||||||
|
}
|
||||||
async function getGroupLink(groupId, btn) {
|
async function getGroupLink(groupId, btn) {
|
||||||
|
const row = document.getElementById('link-row-' + groupId);
|
||||||
|
if (row && row.style.display !== 'none') { row.style.display = 'none'; return; } // toggle off
|
||||||
const orig = btn.textContent;
|
const orig = btn.textContent;
|
||||||
btn.textContent = '…';
|
btn.textContent = '…';
|
||||||
const resp = await fetch(`/api/profiles/{{ profile.id }}/groups/${groupId}/link`, {
|
try {
|
||||||
headers: {'X-Token': _token()},
|
const resp = await fetch(`/api/profiles/{{ profile.id }}/groups/${groupId}/link`, {
|
||||||
});
|
headers: {'X-Token': _token()},
|
||||||
const data = await resp.json();
|
});
|
||||||
if (data.link) {
|
const data = await resp.json();
|
||||||
await navigator.clipboard.writeText(data.link);
|
btn.textContent = orig;
|
||||||
btn.textContent = '✓ Copied';
|
if (data.link) {
|
||||||
} else {
|
const a = document.getElementById('link-url-' + groupId);
|
||||||
btn.textContent = 'No link';
|
a.textContent = data.link;
|
||||||
|
a.href = data.link;
|
||||||
|
row.style.display = '';
|
||||||
|
} else {
|
||||||
|
btn.textContent = 'No link';
|
||||||
|
setTimeout(() => btn.textContent = orig, 2000);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
btn.textContent = 'Error';
|
||||||
|
setTimeout(() => btn.textContent = orig, 2000);
|
||||||
}
|
}
|
||||||
setTimeout(() => btn.textContent = orig, 2000);
|
|
||||||
}
|
}
|
||||||
// ─────────────────────────────────────────────────────────────────────────────
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user