mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-08-07 09:12:57 +02:00
feat: contacts settings tab with suppress + auto-ignore options
Move Manual approval toggle into a new Contacts tab in the global Settings modal and clean up the Contact Management panel (drop the duplicated Settings/Manage Contacts headers, shorten the Existing Contacts blurb). Add two new persisted options gated on Manual approval being ON: Suppress new advert notifications (frontend hides FAB badge + browser notification while the Pending list itself stays populated) and Automatically add new contacts to "Ignored" (advert handler marks the new contact ignored before emitting pending_contact, so the user is silenced end-to-end while contacts remain in the cache for promotion via "To Device"). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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
|
||||
# =============================================================================
|
||||
|
||||
+112
-3
@@ -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();
|
||||
|
||||
|
||||
@@ -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
|
||||
// =============================================================================
|
||||
|
||||
@@ -385,6 +385,9 @@
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsAppearance" type="button">Appearance</button>
|
||||
</li>
|
||||
<li class="nav-item" role="presentation">
|
||||
<button class="nav-link" data-bs-toggle="tab" data-bs-target="#tabSettingsContacts" type="button">Contacts</button>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-content">
|
||||
<!-- Device Settings Tab -->
|
||||
@@ -679,6 +682,45 @@
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div class="tab-pane fade" id="tabSettingsContacts">
|
||||
<table class="table table-sm table-borderless mb-0 align-middle">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td class="ps-0">Manual approval enabled
|
||||
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
|
||||
title="When enabled, new contacts must be manually approved before they can communicate with your node"><i class="bi bi-info-circle"></i></span>
|
||||
</td>
|
||||
<td class="pe-0">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="settManualApproval">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-0" id="settSuppressAdvertNotifsLabel">Suppress new advert notifications
|
||||
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
|
||||
title="Hide the badge over Contact Management and the browser notification when new pending contacts arrive. The Pending Contacts list itself still shows them. Requires Manual approval ON."><i class="bi bi-info-circle"></i></span>
|
||||
</td>
|
||||
<td class="pe-0">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="settSuppressAdvertNotifs">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="ps-0" id="settAutoIgnoreAdvertsLabel">Automatically add new contacts to "Ignored"
|
||||
<span class="badge rounded-pill text-muted" data-bs-toggle="tooltip"
|
||||
title="Every new advert is automatically marked as Ignored — no notifications, no badge. Contacts still appear under Existing Contacts (Cache) and can be promoted with 'To Device'. Requires Manual approval ON."><i class="bi bi-info-circle"></i></span>
|
||||
</td>
|
||||
<td class="pe-0">
|
||||
<div class="form-check form-switch mb-0">
|
||||
<input class="form-check-input" type="checkbox" id="settAutoIgnoreAdverts">
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,34 +4,8 @@
|
||||
|
||||
{% block page_content %}
|
||||
<div id="managePageContent" class="p-3">
|
||||
<!-- Page Header -->
|
||||
<div class="mb-4">
|
||||
<h2 class="mb-1">
|
||||
<i class="bi bi-gear"></i> Settings
|
||||
</h2>
|
||||
<p class="text-muted small mb-0">Configure contact management preferences</p>
|
||||
</div>
|
||||
|
||||
<!-- Manual Approval Settings Section -->
|
||||
<div class="compact-setting">
|
||||
<div class="form-check form-switch mb-0 d-flex align-items-center gap-2">
|
||||
<input class="form-check-input" type="checkbox" role="switch" id="manualApprovalSwitch" style="cursor: pointer; min-width: 3rem; min-height: 1.5rem;">
|
||||
<label class="form-check-label mb-0" for="manualApprovalSwitch" style="cursor: pointer; font-weight: 500;">
|
||||
<span id="switchLabel">Loading...</span>
|
||||
</label>
|
||||
</div>
|
||||
<i class="bi bi-info-circle info-icon"
|
||||
data-bs-toggle="tooltip"
|
||||
data-bs-placement="top"
|
||||
title="When enabled, new contacts must be manually approved before they can communicate with your node"></i>
|
||||
</div>
|
||||
|
||||
<!-- Navigation Section -->
|
||||
<div class="mb-4">
|
||||
<h5 class="mb-3">
|
||||
<i class="bi bi-list-ul"></i> Manage Contacts
|
||||
</h5>
|
||||
|
||||
<!-- Add Contact Card -->
|
||||
<div class="nav-card" onclick="navigateTo('/contacts/add');" style="border-left: 4px solid #198754;">
|
||||
<div>
|
||||
@@ -56,7 +30,7 @@
|
||||
<div class="nav-card" onclick="navigateTo('/contacts/existing');">
|
||||
<div>
|
||||
<h6><i class="bi bi-person-lines-fill"></i> Existing Contacts</h6>
|
||||
<small class="text-muted">View and manage your approved contacts</small>
|
||||
<small class="text-muted">Manage all stored contacts</small>
|
||||
</div>
|
||||
<span class="badge counter-badge counter-ok rounded-pill" id="existingBadge" style="font-size: 1.1rem;">
|
||||
<span class="spinner-border spinner-border-sm" role="status" aria-hidden="true"></span>
|
||||
|
||||
Reference in New Issue
Block a user