diff --git a/app/device_manager.py b/app/device_manager.py index 10e780b..3b44830 100644 --- a/app/device_manager.py +++ b/app/device_manager.py @@ -1156,6 +1156,14 @@ class DeviceManager: pass return False + def _is_auto_ignore_new_adverts_enabled(self) -> bool: + """Check if new adverts should be auto-marked as Ignored (from database).""" + try: + return bool(self.db.get_setting_json('auto_ignore_new_adverts', False)) + except Exception: + pass + return False + async def _on_new_contact(self, event): """Handle new contact discovered. @@ -1247,6 +1255,15 @@ class DeviceManager: source='advert', # cache-only until approved ) + # Auto-ignore: mark as ignored so it never shows up as pending + if self._is_auto_ignore_new_adverts_enabled(): + try: + self.db.set_contact_ignored(pubkey, True) + logger.info(f"Auto-ignored new advert: {name} ({pubkey[:8]}...)") + except Exception as e: + logger.warning(f"Failed to auto-ignore {pubkey[:8]}: {e}") + return # no socket emit -> no badge -> no notification + if self.socketio: self.socketio.emit('pending_contact', { 'public_key': pubkey, diff --git a/app/routes/api.py b/app/routes/api.py index 4d8a26a..0ee177e 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -3753,6 +3753,85 @@ def update_device_settings_api(): }), 500 +@api_bp.route('/contacts/settings', methods=['GET']) +def get_contacts_settings_api(): + """Get contact-management UI/behaviour settings. + + Returns: + { + "success": true, + "settings": { + "manual_add_contacts": bool, + "suppress_advert_notifications": bool, + "auto_ignore_new_adverts": bool + } + } + """ + try: + success, dev_settings = cli.get_device_settings() + manual = bool(dev_settings.get('manual_add_contacts', False)) if success else False + + db = _get_db() + suppress = bool(db.get_setting_json('suppress_advert_notifications', False)) if db else False + auto_ignore = bool(db.get_setting_json('auto_ignore_new_adverts', False)) if db else False + + return jsonify({ + 'success': True, + 'settings': { + 'manual_add_contacts': manual, + 'suppress_advert_notifications': suppress, + 'auto_ignore_new_adverts': auto_ignore, + } + }), 200 + except Exception as e: + logger.error(f"Error getting contacts settings: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + +@api_bp.route('/contacts/settings', methods=['POST']) +def update_contacts_settings_api(): + """Update one or more contact-management settings (partial payload allowed). + + JSON body keys (all optional): + manual_add_contacts: bool -> applied to device + persisted + suppress_advert_notifications: bool -> persisted (frontend-only behaviour) + auto_ignore_new_adverts: bool -> persisted, used by device_manager + """ + try: + data = request.get_json() or {} + if not isinstance(data, dict): + return jsonify({'success': False, 'error': 'Invalid payload'}), 400 + + db = _get_db() + + if 'manual_add_contacts' in data: + val = data['manual_add_contacts'] + if not isinstance(val, bool): + return jsonify({'success': False, 'error': 'manual_add_contacts must be a boolean'}), 400 + success, message = cli.set_manual_add_contacts(val) + if not success: + return jsonify({'success': False, 'error': message}), 500 + + if 'suppress_advert_notifications' in data: + val = data['suppress_advert_notifications'] + if not isinstance(val, bool): + return jsonify({'success': False, 'error': 'suppress_advert_notifications must be a boolean'}), 400 + if db: + db.set_setting_json('suppress_advert_notifications', val) + + if 'auto_ignore_new_adverts' in data: + val = data['auto_ignore_new_adverts'] + if not isinstance(val, bool): + return jsonify({'success': False, 'error': 'auto_ignore_new_adverts must be a boolean'}), 400 + if db: + db.set_setting_json('auto_ignore_new_adverts', val) + + return jsonify({'success': True}), 200 + except Exception as e: + logger.error(f"Error updating contacts settings: {e}") + return jsonify({'success': False, 'error': str(e)}), 500 + + # ============================================================================= # Message Retention Settings # ============================================================================= diff --git a/app/static/js/app.js b/app/static/js/app.js index 969a2af..02bef85 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -451,8 +451,9 @@ function connectChatSocket() { }, 2000); }); - // Real-time pending contact — update badge + // Real-time pending contact — update badge (unless suppressed by user setting) chatSocket.on('pending_contact', () => { + if (window.contactsSettings?.suppress_advert_notifications) return; updatePendingContactsBadge(); }); @@ -2325,6 +2326,7 @@ document.addEventListener('DOMContentLoaded', () => { loadDmRetrySettings(); loadChatSettings(); loadUiSettings(); + loadContactsSettings(); }); settingsModal.addEventListener('shown.bs.modal', () => { settingsModal.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => { @@ -2333,6 +2335,20 @@ document.addEventListener('DOMContentLoaded', () => { }); } + // Contacts tab toggle handlers + document.getElementById('settManualApproval')?.addEventListener('change', (e) => { + saveContactsSetting('manual_add_contacts', e.target.checked, e.target); + }); + document.getElementById('settSuppressAdvertNotifs')?.addEventListener('change', (e) => { + saveContactsSetting('suppress_advert_notifications', e.target.checked, e.target); + }); + document.getElementById('settAutoIgnoreAdverts')?.addEventListener('change', (e) => { + saveContactsSetting('auto_ignore_new_adverts', e.target.checked, e.target); + }); + + // Initial load so suppress flag is available before user opens Settings + loadContactsSettings(); + const dmRetryForm = document.getElementById('dmRetrySettingsForm'); if (dmRetryForm) { dmRetryForm.addEventListener('submit', (e) => { @@ -2606,6 +2622,90 @@ async function handleNotificationToggle() { } } +// ============================================================================= +// Contacts Settings (Settings modal → Contacts tab) +// ============================================================================= + +window.contactsSettings = { + manual_add_contacts: false, + suppress_advert_notifications: false, + auto_ignore_new_adverts: false, +}; + +async function loadContactsSettings() { + try { + const resp = await fetch('/api/contacts/settings'); + if (!resp.ok) return; + const data = await resp.json(); + if (!data.success) return; + const s = data.settings || {}; + window.contactsSettings = { + manual_add_contacts: !!s.manual_add_contacts, + suppress_advert_notifications: !!s.suppress_advert_notifications, + auto_ignore_new_adverts: !!s.auto_ignore_new_adverts, + }; + const m = document.getElementById('settManualApproval'); + const s1 = document.getElementById('settSuppressAdvertNotifs'); + const s2 = document.getElementById('settAutoIgnoreAdverts'); + if (m) m.checked = window.contactsSettings.manual_add_contacts; + if (s1) s1.checked = window.contactsSettings.suppress_advert_notifications; + if (s2) s2.checked = window.contactsSettings.auto_ignore_new_adverts; + applyContactsSettingsEnableState(window.contactsSettings.manual_add_contacts); + // If suppress was just turned on while page open, clear the FAB badge now + if (window.contactsSettings.suppress_advert_notifications) { + updateFabBadge('.fab-contacts', 'fab-badge-pending', 0); + } + } catch (e) { + console.error('Error loading contacts settings:', e); + } +} + +function applyContactsSettingsEnableState(manualOn) { + const s1 = document.getElementById('settSuppressAdvertNotifs'); + const s2 = document.getElementById('settAutoIgnoreAdverts'); + const l1 = document.getElementById('settSuppressAdvertNotifsLabel'); + const l2 = document.getElementById('settAutoIgnoreAdvertsLabel'); + [s1, s2].forEach(el => { + if (!el) return; + el.disabled = !manualOn; + }); + [l1, l2].forEach(el => { + if (!el) return; + el.classList.toggle('text-muted', !manualOn); + }); +} + +async function saveContactsSetting(key, value, inputEl) { + try { + const resp = await fetch('/api/contacts/settings', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ [key]: value }), + }); + const data = await resp.json().catch(() => ({})); + if (!resp.ok || !data.success) { + if (inputEl) inputEl.checked = !value; + showNotification(data.error || 'Failed to save setting', 'danger'); + return; + } + window.contactsSettings[key] = !!value; + if (key === 'manual_add_contacts') { + applyContactsSettingsEnableState(!!value); + } + if (key === 'suppress_advert_notifications' && value) { + updateFabBadge('.fab-contacts', 'fab-badge-pending', 0); + } + if (key === 'suppress_advert_notifications' && !value) { + // Re-fetch real count when re-enabling notifications + updatePendingContactsBadge(); + } + } catch (e) { + console.error('Error saving contacts setting:', e); + if (inputEl) inputEl.checked = !value; + showNotification('Network error saving setting', 'danger'); + } +} + /** * Send browser notification when new messages arrive * @param {number} channelCount - Number of channels with new messages @@ -2679,9 +2779,11 @@ function checkAndNotify() { const dmBadge = document.querySelector('.fab-badge-dm'); const currentDmUnread = dmBadge ? parseInt(dmBadge.textContent) || 0 : 0; - // Get pending contacts count from badge + // Get pending contacts count from badge (forced to 0 when notifications are suppressed) const pendingBadge = document.querySelector('.fab-badge-pending'); - const currentPendingCount = pendingBadge ? parseInt(pendingBadge.textContent) || 0 : 0; + const rawPendingCount = pendingBadge ? parseInt(pendingBadge.textContent) || 0 : 0; + const currentPendingCount = window.contactsSettings?.suppress_advert_notifications + ? 0 : rawPendingCount; // Detect increases (new messages/contacts) const channelIncrease = currentTotalUnread > previousTotalUnread; @@ -4033,6 +4135,13 @@ function updateDmBadges(totalUnread) { */ async function updatePendingContactsBadge() { try { + // Suppress: hide FAB badge entirely, skip browser notification path + if (window.contactsSettings?.suppress_advert_notifications) { + updateFabBadge('.fab-contacts', 'fab-badge-pending', 0); + updateAppBadge(); + return; + } + // Load type filter from localStorage (uses same function as contacts.js) const savedTypes = loadPendingTypeFilter(); diff --git a/app/static/js/contacts.js b/app/static/js/contacts.js index 2725dba..77848c5 100644 --- a/app/static/js/contacts.js +++ b/app/static/js/contacts.js @@ -77,7 +77,6 @@ window.navigateTo = function(url) { // ============================================================================= let currentPage = null; // 'manage', 'pending', 'existing' -let manualApprovalEnabled = false; let pendingContacts = []; let filteredPendingContacts = []; // Filtered pending contacts (for pending page filtering) let existingContacts = []; @@ -210,9 +209,6 @@ function initializePage() { function initManagePage() { console.log('Initializing Management page...'); - // Load settings for manual approval toggle - loadSettings(); - // Load contact counts for badges loadContactCounts(); @@ -224,12 +220,6 @@ function initManagePage() { } function attachManageEventListeners() { - // Manual approval toggle - const approvalSwitch = document.getElementById('manualApprovalSwitch'); - if (approvalSwitch) { - approvalSwitch.addEventListener('change', handleApprovalToggle); - } - // Cleanup preview button const cleanupPreviewBtn = document.getElementById('cleanupPreviewBtn'); if (cleanupPreviewBtn) { @@ -945,82 +935,6 @@ function attachExistingEventListeners() { } } -// ============================================================================= -// Settings Management (shared) -// ============================================================================= - -async function loadSettings() { - try { - const response = await fetch('/api/device/settings'); - const data = await response.json(); - - if (data.success) { - manualApprovalEnabled = data.settings.manual_add_contacts || false; - updateApprovalUI(manualApprovalEnabled); - } else { - console.error('Failed to load settings:', data.error); - showToast('Failed to load settings', 'danger'); - } - } catch (error) { - console.error('Error loading settings:', error); - showToast('Network error loading settings', 'danger'); - } -} - -async function handleApprovalToggle(event) { - const enabled = event.target.checked; - - try { - const response = await fetch('/api/device/settings', { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - manual_add_contacts: enabled - }) - }); - - const data = await response.json(); - - if (data.success) { - manualApprovalEnabled = enabled; - updateApprovalUI(enabled); - showToast( - enabled ? 'Manual approval enabled' : 'Manual approval disabled', - 'success' - ); - } else { - console.error('Failed to update setting:', data.error); - showToast('Failed to update setting: ' + data.error, 'danger'); - - // Revert toggle on failure - event.target.checked = !enabled; - } - } catch (error) { - console.error('Error updating setting:', error); - showToast('Network error updating setting', 'danger'); - - // Revert toggle on failure - event.target.checked = !enabled; - } -} - -function updateApprovalUI(enabled) { - const switchEl = document.getElementById('manualApprovalSwitch'); - const labelEl = document.getElementById('switchLabel'); - - if (switchEl) { - switchEl.checked = enabled; - } - - if (labelEl) { - labelEl.textContent = enabled - ? 'Manual approval enabled' - : 'Automatic approval (default)'; - } -} - // ============================================================================= // Protected Contacts Management // ============================================================================= diff --git a/app/templates/base.html b/app/templates/base.html index 1d64f91..ae76df8 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -385,6 +385,9 @@ +
@@ -679,6 +682,45 @@
+
+ + + + + + + + + + + + + + + +
Manual approval enabled + + +
+ +
+
Suppress new advert notifications + + +
+ +
+
Automatically add new contacts to "Ignored" + + +
+ +
+
+
diff --git a/app/templates/contacts-manage.html b/app/templates/contacts-manage.html index 5e529a9..0ca5511 100644 --- a/app/templates/contacts-manage.html +++ b/app/templates/contacts-manage.html @@ -4,34 +4,8 @@ {% block page_content %}
- -
-

- Settings -

-

Configure contact management preferences

-
- - -
-
- - -
- -
-
-
- Manage Contacts -
-