feat: Add notification badges to FAB buttons

- 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 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-01-04 11:51:07 +01:00
parent 009d3254b8
commit 7cee262971
3 changed files with 133 additions and 48 deletions
+36
View File
@@ -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 {
+82 -20
View File
@@ -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<number>} 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];
}
+15 -28
View File
@@ -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);
}
});
}
});
</script>