From da2e39c6b985be0a7b9fae3d0fa826912acf1923 Mon Sep 17 00:00:00 2001 From: agessaman Date: Sun, 29 Mar 2026 17:13:01 -0700 Subject: [PATCH] Enhance mobile contact management UI and functionality - Introduced a mobile-friendly layout for the contacts section, including a new mobile stack for displaying contact cards. - Added sorting options for contacts on mobile devices, allowing users to sort by various criteria such as name, device type, and distance. - Implemented a select-all checkbox for mobile view to improve user interaction with contact selection. - Updated event listeners to handle changes in selection and sorting, ensuring a seamless experience across devices. These changes improve usability and accessibility for mobile users in the web viewer. --- modules/web_viewer/templates/contacts.html | 199 +++++++++++++++++++-- 1 file changed, 180 insertions(+), 19 deletions(-) diff --git a/modules/web_viewer/templates/contacts.html b/modules/web_viewer/templates/contacts.html index 49e150f..d1aae40 100644 --- a/modules/web_viewer/templates/contacts.html +++ b/modules/web_viewer/templates/contacts.html @@ -14,6 +14,43 @@ justify-content: center; border-radius: 0.25rem; } + + #contacts-mobile-stack { + display: flex; + flex-direction: column; + gap: 0.75rem; + overflow-x: hidden; + } + + .contact-mobile-card { + background-color: var(--card-bg); + color: var(--text-color); + border: 1px solid var(--border-color); + border-radius: 0.375rem; + padding: 0.75rem 0.85rem; + min-width: 0; + } + + .contact-mobile-card .contact-mobile-device { + max-width: 65%; + } + + .contact-mobile-card .contact-mobile-device .badge { + font-size: 0.75rem; + white-space: normal; + text-align: right; + line-height: 1.2; + } + + .contact-mobile-card .contact-mobile-hops-adverts { + min-width: 0; + } + + .contact-mobile-card .contact-mobile-hops-adverts .badge { + font-size: 0.75rem; + line-height: 1.25; + padding: 0.35em 0.55em; + } {% endblock %} @@ -122,7 +159,7 @@
-
+
@@ -189,7 +226,36 @@
-
+
+
+ + +
+
+ + +
+
+
@@ -235,6 +301,9 @@
+
+
Loading contacts data...
+
@@ -361,27 +430,42 @@ class ModernContactsManager { this.showBulkDeleteConfirmation(); }); - const contactsTable = document.querySelector('.table-responsive table'); - if (contactsTable) { - contactsTable.addEventListener('change', (e) => { - if (e.target.id === 'contacts-select-all') { - const checked = e.target.checked; + const listPanel = document.getElementById('contacts-list-panel'); + if (listPanel) { + listPanel.addEventListener('change', (e) => { + const t = e.target; + if (t.id === 'contacts-select-all' || t.id === 'contacts-select-all-mobile') { + const checked = t.checked; this.filteredData.forEach(c => { if (checked) this.selectedContactIds.add(c.user_id); else this.selectedContactIds.delete(c.user_id); }); - contactsTable.querySelectorAll('.contact-row-checkbox').forEach(cb => cb.checked = checked); + listPanel.querySelectorAll('.contact-row-checkbox').forEach(cb => { cb.checked = checked; }); this.updateBulkDeleteButton(); this.updateSelectAllCheckbox(); - } else if (e.target.classList.contains('contact-row-checkbox')) { - const userId = e.target.dataset.userId; - if (e.target.checked) this.selectedContactIds.add(userId); + } else if (t.classList.contains('contact-row-checkbox')) { + const userId = t.dataset.userId; + if (t.checked) this.selectedContactIds.add(userId); else this.selectedContactIds.delete(userId); this.updateBulkDeleteButton(); this.updateSelectAllCheckbox(); } }); } + + const mobileSort = document.getElementById('contacts-mobile-sort'); + if (mobileSort) { + mobileSort.addEventListener('change', () => { + const v = mobileSort.value.split('|'); + if (v.length === 2) { + this.sortColumn = v[0]; + this.sortDirection = v[1]; + this.applySort(); + this.renderContactsData(); + this.updateSortIcons(); + } + }); + } // Add click handlers for sortable columns document.querySelectorAll('.sortable').forEach(header => { @@ -495,6 +579,16 @@ class ModernContactsManager { icon.className = this.sortDirection === 'asc' ? 'fas fa-sort-up' : 'fas fa-sort-down'; } } + this.syncMobileSortSelect(); + } + + syncMobileSortSelect() { + const sel = document.getElementById('contacts-mobile-sort'); + if (!sel) return; + const val = `${this.sortColumn}|${this.sortDirection}`; + if ([...sel.options].some(o => o.value === val)) { + sel.value = val; + } } applySearch() { @@ -527,12 +621,67 @@ class ModernContactsManager { }); } + renderContactCardHtml(contact) { + const uidEsc = this.escapeHtml(contact.user_id); + const uidJs = contact.user_id.replace(/'/g, "\\'"); + const nameJs = (contact.username || 'Unknown').replace(/'/g, "\\'"); + const checked = this.selectedContactIds.has(contact.user_id) ? 'checked' : ''; + const hasGeo = !!(contact.latitude && contact.longitude && contact.latitude !== 0 && contact.longitude !== 0); + const starLabel = contact.is_starred ? 'Unstar contact' : 'Star contact'; + const geoMenuItem = hasGeo + ? `
  • ` + : ''; + return ` +
    +
    + +
    +
    +
    +
    ${contact.username || 'Unknown'}
    + ${contact.user_id ? contact.user_id.substring(0, 16) + '...' : 'Unknown'} +
    +
    ${this.formatDeviceType(contact)}
    +
    +
    ${this.formatLocation(contact)}
    +
    ${this.formatDistance(contact)} · ${this.formatSignal(contact)}
    +
    ${this.formatTimestamp(contact.first_heard)} · ${this.formatTimeAgo(contact.last_seen)}
    +
    +
    +
    ${this.formatHops(contact)}
    +
    + ${contact.advert_count || 0} + adverts +
    +
    + +
    +
    +
    +
    + `; + } + renderContactsData() { const tbody = document.getElementById('contacts-table-body'); - + const mobileStack = document.getElementById('contacts-mobile-stack'); + if (this.filteredData.length === 0) { - const message = this.searchTerm ? - `No contacts match "${this.searchTerm}"` : + const message = this.searchTerm ? + `No contacts match "${this.searchTerm}"` : 'No contacts data available'; tbody.innerHTML = ` @@ -541,11 +690,14 @@ class ModernContactsManager { `; + if (mobileStack) { + mobileStack.innerHTML = `
    ${message}
    `; + } this.updateBulkDeleteButton(); this.updateSelectAllCheckbox(); return; } - + tbody.innerHTML = this.filteredData.map(contact => ` @@ -585,7 +737,11 @@ class ModernContactsManager { `).join(''); - + + if (mobileStack) { + mobileStack.innerHTML = this.filteredData.map(contact => this.renderContactCardHtml(contact)).join(''); + } + // Setup path tooltips after rendering this.setupPathTooltips(); this.updateBulkDeleteButton(); @@ -609,12 +765,17 @@ class ModernContactsManager { updateSelectAllCheckbox() { const selectAll = document.getElementById('contacts-select-all'); - if (!selectAll) return; + const selectAllMobile = document.getElementById('contacts-select-all-mobile'); const visibleIds = new Set(this.filteredData.map(c => c.user_id)); const allSelected = visibleIds.size > 0 && [...visibleIds].every(id => this.selectedContactIds.has(id)); const someSelected = [...visibleIds].some(id => this.selectedContactIds.has(id)); - selectAll.checked = allSelected; - selectAll.indeterminate = someSelected && !allSelected; + const sync = (el) => { + if (!el) return; + el.checked = allSelected; + el.indeterminate = someSelected && !allSelected; + }; + sync(selectAll); + sync(selectAllMobile); } updateStatistics() {