From 7cee262971855e2472a674ba6f8764ac77a5502a Mon Sep 17 00:00:00 2001 From: MarekWo Date: Sun, 4 Jan 2026 11:51:07 +0100 Subject: [PATCH] feat: Add notification badges to FAB buttons MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add badge for pending contacts on Contact Management FAB button (orange) - Move DM badge from notification bell to Direct Messages FAB button (green) - Add universal updateFabBadge() function for all FAB badges - Add updatePendingContactsBadge() function with localStorage type filter support - Update badges every 10 seconds with auto-refresh - Update badges when modals are closed Frontend changes: - New CSS classes: .fab-badge, .fab-badge-pending, .fab-badge-dm - position: relative added to .fab for badge positioning - Removed DM badge code from notification bell - Added modal event handlers for badge updates Backend: No changes (uses existing /api/contacts/pending endpoint with types parameter) This improves UX by showing notification counts directly on relevant FAB buttons instead of crowding the notification bell. DM badge moved from bell to DM button, and new pending contacts badge added to Contact Management button. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.5 --- app/static/css/style.css | 36 ++++++++++++++ app/static/js/app.js | 102 +++++++++++++++++++++++++++++++-------- app/templates/index.html | 43 ++++++----------- 3 files changed, 133 insertions(+), 48 deletions(-) diff --git a/app/static/css/style.css b/app/static/css/style.css index 3ffe7ac..0eecdfc 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -595,6 +595,7 @@ main { } .fab { + position: relative; /* For badge positioning */ width: 56px; height: 56px; border-radius: 50%; @@ -627,6 +628,41 @@ main { color: white; } +/* FAB Badge (base style for all badges on FAB buttons) */ +.fab-badge { + position: absolute; + top: -4px; + right: -4px; + min-width: 20px; + height: 20px; + border-radius: 10px; + padding: 2px 6px; + font-size: 0.65rem; + font-weight: bold; + line-height: 1.4; + text-align: center; + color: white; + display: none; + z-index: 1; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.2); +} + +/* FAB Badge for Pending Contacts (orange/amber) */ +.fab-badge-pending { + background-color: #fd7e14; /* Bootstrap orange */ +} + +/* FAB Badge for Direct Messages (green) */ +.fab-badge-dm { + background-color: #198754; /* Bootstrap success green */ +} + +/* Animation on hover */ +.fab:hover .fab-badge { + transform: scale(1.1); + transition: transform 0.2s ease; +} + /* Mobile optimization */ @media (max-width: 768px) { .fab-container { diff --git a/app/static/js/app.js b/app/static/js/app.js index 70ba4d6..ca06d8e 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -80,6 +80,9 @@ document.addEventListener('DOMContentLoaded', async function() { // Now load other data (can run in parallel) loadArchiveList(); loadMessages(); + + // Initial badge updates + updatePendingContactsBadge(); loadStatus(); // Setup auto-refresh AFTER channels are loaded @@ -609,6 +612,7 @@ function setupAutoRefresh() { await checkForUpdates(); await checkDmUpdates(); // Also check for DM updates + await updatePendingContactsBadge(); // Also check for pending contacts }, checkInterval); console.log(`Intelligent auto-refresh enabled: checking every ${checkInterval / 1000}s`); @@ -945,6 +949,35 @@ function updateNotificationBell(count) { } } +/** + * Update FAB button badge (universal function for all FAB badges) + * @param {string} fabSelector - CSS selector for FAB button (e.g., '.fab-dm', '.fab-contacts') + * @param {string} badgeClass - Badge class name (e.g., 'fab-badge-dm', 'fab-badge-pending') + * @param {number} count - Number to display (0 = hide badge) + */ +function updateFabBadge(fabSelector, badgeClass, count) { + const fabButton = document.querySelector(fabSelector); + if (!fabButton) return; + + let badge = fabButton.querySelector(`.${badgeClass}`); + + if (count > 0) { + // Show badge + if (!badge) { + badge = document.createElement('span'); + badge.className = `fab-badge ${badgeClass}`; + fabButton.appendChild(badge); + } + badge.textContent = count > 99 ? '99+' : count; + badge.style.display = 'inline-block'; + } else { + // Hide badge + if (badge) { + badge.style.display = 'none'; + } + } +} + /** * Setup emoji picker */ @@ -1367,29 +1400,58 @@ function updateDmBadges(totalUnread) { } } - // Update notification bell (secondary badge) - const bellContainer = document.getElementById('notificationBell'); - if (!bellContainer) return; + // Update FAB badge (green badge on Direct Messages button) + updateFabBadge('.fab-dm', 'fab-badge-dm', totalUnread); +} - let dmBadge = bellContainer.querySelector('.notification-badge-dm'); +/** + * Update pending contacts badge on Contact Management FAB button + * Fetches count from API using type filter from localStorage + */ +async function updatePendingContactsBadge() { + try { + // Load type filter from localStorage (uses same function as contacts.js) + const savedTypes = loadPendingTypeFilter(); - if (totalUnread > 0) { - if (!dmBadge) { - dmBadge = document.createElement('span'); - dmBadge.className = 'notification-badge-dm'; - bellContainer.appendChild(dmBadge); + // Build query string with types parameter + const params = new URLSearchParams(); + savedTypes.forEach(type => params.append('types', type)); + + // Fetch pending count with type filter + const response = await fetch(`/api/contacts/pending?${params.toString()}`); + if (!response.ok) return; + + const data = await response.json(); + + if (data.success) { + const count = data.pending?.length || 0; + // Update FAB badge (orange badge on Contact Management button) + updateFabBadge('.fab-contacts', 'fab-badge-pending', count); } - dmBadge.textContent = totalUnread > 99 ? '99+' : totalUnread; - dmBadge.style.display = 'inline-block'; - - // Animate bell - const bellIcon = bellContainer.querySelector('i'); - if (bellIcon) { - bellIcon.classList.add('bell-ring'); - setTimeout(() => bellIcon.classList.remove('bell-ring'), 1000); - } - } else if (dmBadge) { - dmBadge.style.display = 'none'; + } catch (error) { + console.error('Error updating pending contacts badge:', error); } } +/** + * Load pending contacts type filter from localStorage. + * This is a duplicate of the function in contacts.js for use in app.js + * @returns {Array} Array of contact types (default: [1] for CLI only) + */ +function loadPendingTypeFilter() { + try { + const stored = localStorage.getItem('pendingContactsTypeFilter'); + if (stored) { + const types = JSON.parse(stored); + // Validate: must be array of valid types + if (Array.isArray(types) && types.every(t => [1, 2, 3, 4].includes(t))) { + return types; + } + } + } catch (e) { + console.error('Failed to load pending type filter from localStorage:', e); + } + // Default: CLI only (most common use case) + return [1]; +} + diff --git a/app/templates/index.html b/app/templates/index.html index 4618a13..05fd5c2 100644 --- a/app/templates/index.html +++ b/app/templates/index.html @@ -202,34 +202,9 @@ await loadDmLastSeenTimestampsFromServer(); } - // Fetch latest DM conversations with unread counts - const response = await fetch('/api/dm/conversations'); - if (response.ok) { - const data = await response.json(); - - // Calculate total unread by summing unread counts from all conversations - let totalUnread = 0; - if (data.conversations && Array.isArray(data.conversations)) { - totalUnread = data.conversations.reduce((sum, conv) => sum + (conv.unread || 0), 0); - } - - // Update the green DM badge on notification bell - const bellContainer = document.getElementById('notificationBell'); - if (bellContainer) { - let dmBadge = bellContainer.querySelector('.notification-badge-dm'); - - if (totalUnread > 0) { - if (!dmBadge) { - dmBadge = document.createElement('span'); - dmBadge.className = 'notification-badge-dm'; - bellContainer.appendChild(dmBadge); - } - dmBadge.textContent = totalUnread > 99 ? '99+' : totalUnread; - dmBadge.style.display = 'inline-block'; - } else if (dmBadge) { - dmBadge.style.display = 'none'; - } - } + // Trigger DM badge update (now handled by app.js on FAB button) + if (typeof checkDmUpdates === 'function') { + await checkDmUpdates(); } } catch (error) { console.error('Error updating DM badges:', error); @@ -244,6 +219,18 @@ contactsFrame.src = contactsFrame.src; } }); + + // Update pending contacts badge when modal is closed + contactsModal.addEventListener('hidden.bs.modal', async function () { + try { + // Trigger pending contacts badge update (handled by app.js on FAB button) + if (typeof updatePendingContactsBadge === 'function') { + await updatePendingContactsBadge(); + } + } catch (error) { + console.error('Error updating pending contacts badge:', error); + } + }); } });