diff --git a/assets/translations/en.json b/assets/translations/en.json index b3536897eb..777c6f13c3 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -994,11 +994,13 @@ "filter": { "add": "Add filter", "add-title": "Add Notification Filter", + "allow-permanent": "Allow permanent", "allowed-urgencies-label": "Allowed urgencies", "flag": { "history": "history", "sound": "sound", - "toast": "toast" + "toast": "toast", + "permanent": "permanent" }, "flags-label": "When matched", "match-label": "App", @@ -1697,6 +1699,10 @@ } }, "notifications": { + "allow-permanent": { + "description": "When disabled permanent notifications expire", + "label": "Allow Permanent" + }, "allowed-urgencies": { "description": "Only show external notifications at the selected urgency levels", "label": "Allowed Urgencies" diff --git a/src/app/application.cpp b/src/app/application.cpp index f44e7e0bf9..28742ba42d 100644 --- a/src/app/application.cpp +++ b/src/app/application.cpp @@ -1542,11 +1542,13 @@ void Application::initUi() { m_notificationToast.initialize(m_wayland, &m_configService, &m_notificationManager, &m_renderContext, &m_httpClient); m_configService.addReloadCallback([this]() { m_notificationToast.onConfigReload(); }); - auto applyNotificationFilterConfig = [this]() { - m_notificationManager.setFilters(m_configService.config().notification.filters); + auto applyNotificationConfig = [this]() { + const auto notification = m_configService.config().notification; + m_notificationManager.setFilters(notification.filters); + m_notificationManager.setAllowPermanent(notification.allowPermanent); }; - applyNotificationFilterConfig(); - m_configService.addReloadCallback(applyNotificationFilterConfig); + applyNotificationConfig(); + m_configService.addReloadCallback(applyNotificationConfig); m_configService.setNotificationManager(&m_notificationManager); m_notificationManager.setSoundPlayer(m_soundPlayer.get()); diff --git a/src/config/config_overrides.cpp b/src/config/config_overrides.cpp index a67a4dba50..419050ad12 100644 --- a/src/config/config_overrides.cpp +++ b/src/config/config_overrides.cpp @@ -522,6 +522,7 @@ namespace { row.insert_or_assign("show_toast", item.showToast); row.insert_or_assign("save_history", item.saveHistory); row.insert_or_assign("play_sound", item.playSound); + row.insert_or_assign("allow_permanent", item.allowPermanent); if (!item.allowedUrgencies.empty()) { toml::array urgencies; for (const auto& urgency : item.allowedUrgencies) { diff --git a/src/config/config_types.h b/src/config/config_types.h index 57af3f0abc..e673930b36 100644 --- a/src/config/config_types.h +++ b/src/config/config_types.h @@ -216,6 +216,7 @@ struct NotificationFilterConfig { bool showToast = true; bool saveHistory = true; bool playSound = true; + bool allowPermanent = true; /// Empty = allow low, normal, and critical. Otherwise only listed urgencies pass this filter. std::vector allowedUrgencies; @@ -607,6 +608,7 @@ struct NotificationConfig { int offsetY = 8; // absolute vertical margin from the screen edge std::vector monitors; bool collapseOnDismiss = true; + bool allowPermanent = true; std::vector filters; bool operator==(const NotificationConfig&) const = default; diff --git a/src/config/schema/config_schema.cpp b/src/config/schema/config_schema.cpp index 6edf0fb918..6ba63e1ba4 100644 --- a/src/config/schema/config_schema.cpp +++ b/src/config/schema/config_schema.cpp @@ -203,6 +203,7 @@ namespace noctalia::config::schema { field(&NotificationFilterConfig::showToast, "show_toast"), field(&NotificationFilterConfig::saveHistory, "save_history"), field(&NotificationFilterConfig::playSound, "play_sound"), + field(&NotificationFilterConfig::allowPermanent, "allow_permanent"), field(&NotificationFilterConfig::allowedUrgencies, "allowed_urgencies"), custom( "allow_critical", [](const toml::table&, NotificationFilterConfig&, std::string_view, Diagnostics&) {}, @@ -229,6 +230,7 @@ namespace noctalia::config::schema { field(&NotificationConfig::offsetY, "offset_y"), field(&NotificationConfig::monitors, "monitors"), field(&NotificationConfig::collapseOnDismiss, "collapse_on_dismiss"), + field(&NotificationConfig::allowPermanent, "allow_permanent"), custom( "blacklist", [](const toml::table& tbl, NotificationConfig& out, std::string_view, Diagnostics&) { diff --git a/src/notification/notification_filter.cpp b/src/notification/notification_filter.cpp index 8b2a26d090..795077fc05 100644 --- a/src/notification/notification_filter.cpp +++ b/src/notification/notification_filter.cpp @@ -137,6 +137,7 @@ ResolvedNotificationFilter resolveNotificationFilter( .showToast = filter.showToast, .saveHistory = filter.saveHistory, .playSound = filter.playSound, + .allowPermanent = filter.allowPermanent, .allowedUrgencies = normalizeAllowedUrgencies(filter.allowedUrgencies), .matched = true, }; diff --git a/src/notification/notification_filter.h b/src/notification/notification_filter.h index f5c7388216..efce162b28 100644 --- a/src/notification/notification_filter.h +++ b/src/notification/notification_filter.h @@ -19,6 +19,7 @@ struct ResolvedNotificationFilter { bool showToast = true; bool saveHistory = true; bool playSound = true; + bool allowPermanent = true; /// Empty = all urgencies allowed for this filter. std::unordered_set allowedUrgencies; bool matched = false; diff --git a/src/notification/notification_manager.cpp b/src/notification/notification_manager.cpp index 66388214a8..6be6a09b30 100644 --- a/src/notification/notification_manager.cpp +++ b/src/notification/notification_manager.cpp @@ -195,6 +195,25 @@ uint32_t NotificationManager::addOrReplace( ); }; + if (timeout == 0) { + const auto resolved = resolveNotificationFilter( + m_filters, + NotificationFilterFields{ + .appName = appName, + .category = category.has_value() ? std::optional{*category} : std::nullopt, + .desktopEntry = desktopEntry.has_value() ? std::optional{*desktopEntry} : std::nullopt, + } + ); + + kLog.debug( + "notification allowPermanent global-allow={} filter-matched={} filter-allow={}", m_allowPermanent, + resolved.matched, resolved.allowPermanent + ); + if ((resolved.matched && !resolved.allowPermanent) || (!resolved.matched && !m_allowPermanent)) { + timeout = kDefaultNotificationTimeout; + } + } + const ExternalNotificationDispatch externalDispatch = origin == NotificationOrigin::External ? evaluateExternalDispatch(urgency, appName, category, desktopEntry, transient) : ExternalNotificationDispatch{}; @@ -304,6 +323,7 @@ uint32_t NotificationManager::addOrReplace( const auto& n = m_notifications.back(); logNotification(n, "added"); + if (origin == NotificationOrigin::External ? externalDispatch.saveHistory : shouldTrackHistory(n.origin, n.urgency, n.transient)) { const bool hadUnreadBefore = computeHasUnreadNotificationHistory(); @@ -634,6 +654,8 @@ void NotificationManager::setStateCallback(StateCallback callback) { m_stateCall void NotificationManager::setSoundPlayer(SoundPlayer* soundPlayer) { m_soundPlayer = soundPlayer; } +void NotificationManager::setAllowPermanent(bool allowPermanent) { m_allowPermanent = allowPermanent; } + bool NotificationManager::hasUnreadNotificationHistory() const noexcept { return computeHasUnreadNotificationHistory(); } diff --git a/src/notification/notification_manager.h b/src/notification/notification_manager.h index d582ff2fbf..624e9d5f0f 100644 --- a/src/notification/notification_manager.h +++ b/src/notification/notification_manager.h @@ -109,6 +109,7 @@ class NotificationManager { [[nodiscard]] bool toggleDoNotDisturb(); void setStateCallback(StateCallback callback); void setSoundPlayer(class SoundPlayer* soundPlayer); + void setAllowPermanent(bool allowPermanent); // Bar indicator: true when at least one notification was added since the user last // viewed the notification history (control center notifications tab). @@ -158,5 +159,6 @@ class NotificationManager { uint32_t m_nextId{1}; std::uint64_t m_changeSerial{0}; bool m_doNotDisturb = false; + bool m_allowPermanent = true; class SoundPlayer* m_soundPlayer = nullptr; }; diff --git a/src/shell/settings/settings_content_common.cpp b/src/shell/settings/settings_content_common.cpp index 6b3097e90c..b77bfd258e 100644 --- a/src/shell/settings/settings_content_common.cpp +++ b/src/shell/settings/settings_content_common.cpp @@ -324,6 +324,9 @@ namespace settings { if (filter.playSound) { parts.emplace_back(i18n::tr("settings.notifications.filter.flag.sound")); } + if (filter.allowPermanent) { + parts.emplace_back(i18n::tr("settings.notifications.filter.flag.permanent")); + } if (!filter.allowedUrgencies.empty()) { std::vector urgencyLabels; urgencyLabels.reserve(filter.allowedUrgencies.size()); diff --git a/src/shell/settings/settings_content_notification_filter.cpp b/src/shell/settings/settings_content_notification_filter.cpp index 4b16e1b525..1b847aea40 100644 --- a/src/shell/settings/settings_content_notification_filter.cpp +++ b/src/shell/settings/settings_content_notification_filter.cpp @@ -158,6 +158,13 @@ namespace settings { persist(); } ); + addToggleRow( + *flagsBlock, scale, i18n::tr("settings.notifications.filter.allow-permanent"), row.allowPermanent, + [&row, persist](bool value) { + row.allowPermanent = value; + persist(); + } + ); body->addChild(std::move(flagsBlock)); parent.addChild(std::move(body)); diff --git a/src/shell/settings/settings_registry.cpp b/src/shell/settings/settings_registry.cpp index 73a54a8263..9eea2f6f1d 100644 --- a/src/shell/settings/settings_registry.cpp +++ b/src/shell/settings/settings_registry.cpp @@ -2157,6 +2157,11 @@ namespace settings { tr("settings.schema.notifications.collapse-on-dismiss.description"), {"notification", "collapse_on_dismiss"}, ToggleSetting{cfg.notification.collapseOnDismiss}, "reorder stack slide" )); + entries.push_back(makeEntry( + SettingsSection::Notifications, "general", tr("settings.schema.notifications.allow-permanent.label"), + tr("settings.schema.notifications.allow-permanent.description"), {"notification", "allow_permanent"}, + ToggleSetting{cfg.notification.allowPermanent}, "allow permanent notifications" + )); entries.push_back(makeEntry( SettingsSection::Notifications, "toasts", tr("settings.schema.notifications.monitors.label"), tr("settings.schema.notifications.monitors.description"), {"notification", "monitors"}, diff --git a/src/shell/settings/settings_window_popups.cpp b/src/shell/settings/settings_window_popups.cpp index 7eab7dc594..58db430c8c 100644 --- a/src/shell/settings/settings_window_popups.cpp +++ b/src/shell/settings/settings_window_popups.cpp @@ -871,6 +871,7 @@ void SettingsWindow::openNotificationFilterCreateEditor() { .showToast = true, .saveHistory = true, .playSound = true, + .allowPermanent = true, .allowedUrgencies = {}, }); diff --git a/tests/config_schema_roundtrip_test.cpp b/tests/config_schema_roundtrip_test.cpp index 808e6f763c..1d5908b237 100644 --- a/tests/config_schema_roundtrip_test.cpp +++ b/tests/config_schema_roundtrip_test.cpp @@ -297,6 +297,7 @@ location = "https://example.invalid/bad" 6, {"DP-2"}, false, + true, {NotificationFilterConfig{ .name = "discord", .enabled = true, @@ -304,6 +305,7 @@ location = "https://example.invalid/bad" .showToast = false, .saveHistory = false, .playSound = false, + .allowPermanent = false, .allowedUrgencies = {"normal", "critical"}, }}, };