diff --git a/app/static/js/contacts.js b/app/static/js/contacts.js
index 03e96e2..e5d0617 100644
--- a/app/static/js/contacts.js
+++ b/app/static/js/contacts.js
@@ -1650,11 +1650,12 @@ function createExistingContactCard(contact, index) {
infoRow.appendChild(nameDiv);
infoRow.appendChild(typeBadge);
- // Public key row
+ // Public key row (clickable to copy)
const keyDiv = document.createElement('div');
- keyDiv.className = 'contact-key';
+ keyDiv.className = 'contact-key clickable-key';
keyDiv.textContent = contact.public_key_prefix;
- keyDiv.title = 'Public Key Prefix';
+ keyDiv.title = 'Click to copy';
+ keyDiv.onclick = () => copyToClipboard(contact.public_key_prefix, keyDiv);
// Last advert row (with activity status indicator)
const lastAdvertDiv = document.createElement('div');
@@ -1700,14 +1701,6 @@ function createExistingContactCard(contact, index) {
const actionsDiv = document.createElement('div');
actionsDiv.className = 'd-flex gap-2 mt-2';
- // Copy key button
- const copyBtn = document.createElement('button');
- copyBtn.className = 'btn btn-sm btn-outline-secondary';
- copyBtn.innerHTML = ' Copy Key';
- copyBtn.onclick = () => copyContactKey(contact.public_key_prefix, copyBtn);
-
- actionsDiv.appendChild(copyBtn);
-
// Map button (only if GPS coordinates available)
if (contact.adv_lat && contact.adv_lon && (contact.adv_lat !== 0 || contact.adv_lon !== 0)) {
const mapBtn = document.createElement('button');
@@ -1748,25 +1741,63 @@ function createExistingContactCard(contact, index) {
return card;
}
-function copyContactKey(publicKeyPrefix, buttonEl) {
- navigator.clipboard.writeText(publicKeyPrefix).then(() => {
- // Visual feedback
- const originalHTML = buttonEl.innerHTML;
- buttonEl.innerHTML = ' Copied!';
- buttonEl.classList.remove('btn-outline-secondary');
- buttonEl.classList.add('btn-success');
+/**
+ * Copy text to clipboard with fallback for HTTP contexts.
+ * @param {string} text - Text to copy
+ * @param {HTMLElement} element - Element for visual feedback
+ */
+function copyToClipboard(text, element) {
+ const originalText = element.textContent;
- setTimeout(() => {
- buttonEl.innerHTML = originalHTML;
- buttonEl.classList.remove('btn-success');
- buttonEl.classList.add('btn-outline-secondary');
- }, 2000);
+ // Try modern clipboard API first (requires HTTPS)
+ if (navigator.clipboard && navigator.clipboard.writeText) {
+ navigator.clipboard.writeText(text).then(() => {
+ showCopyFeedback(element, originalText);
+ }).catch(() => {
+ // Fallback to legacy method
+ legacyCopy(text, element, originalText);
+ });
+ } else {
+ // Fallback for HTTP contexts
+ legacyCopy(text, element, originalText);
+ }
+}
- showToast('Key copied to clipboard', 'info');
- }).catch(err => {
+/**
+ * Legacy copy method using execCommand (works on HTTP).
+ */
+function legacyCopy(text, element, originalText) {
+ const textArea = document.createElement('textarea');
+ textArea.value = text;
+ textArea.style.position = 'fixed';
+ textArea.style.left = '-9999px';
+ document.body.appendChild(textArea);
+ textArea.select();
+
+ try {
+ document.execCommand('copy');
+ showCopyFeedback(element, originalText);
+ } catch (err) {
console.error('Failed to copy:', err);
- showToast('Failed to copy to clipboard', 'danger');
- });
+ showToast('Failed to copy', 'danger');
+ }
+
+ document.body.removeChild(textArea);
+}
+
+/**
+ * Show visual feedback after successful copy.
+ */
+function showCopyFeedback(element, originalText) {
+ element.textContent = 'Copied!';
+ element.classList.add('copied');
+
+ setTimeout(() => {
+ element.textContent = originalText;
+ element.classList.remove('copied');
+ }, 1500);
+
+ showToast('Key copied to clipboard', 'info');
}
function showDeleteModal(contact) {
diff --git a/app/templates/contacts_base.html b/app/templates/contacts_base.html
index e050d75..7362010 100644
--- a/app/templates/contacts_base.html
+++ b/app/templates/contacts_base.html
@@ -73,6 +73,24 @@
margin-bottom: 0.75rem;
}
+ .contact-key.clickable-key {
+ cursor: pointer;
+ transition: color 0.15s, background-color 0.15s;
+ padding: 0.15rem 0.3rem;
+ margin-left: -0.3rem;
+ border-radius: 0.25rem;
+ }
+
+ .contact-key.clickable-key:hover {
+ color: #0d6efd;
+ background-color: #e7f1ff;
+ }
+
+ .contact-key.clickable-key.copied {
+ color: #198754;
+ background-color: #d1e7dd;
+ }
+
.empty-state {
text-align: center;
padding: 1.5rem 1rem;