Fix critical iOS notification service issues

- Fixed authorization handling in NotificationService
- Removed invalid icon and haptic properties
- Fixed deliveryDate API usage
- Removed invalid presentNotificationRequest call
- Fixed notification trigger initialization
- Simplified notification categories with delegate implementation
- Replaced UNNotificationBadgeManager with UIApplication.shared.applicationIconBadgeNumber
- Eliminated code duplication in badge update logic
- Fixed NotificationPreferencesStore JSON encoding/decoding
This commit is contained in:
2026-03-30 23:54:39 -04:00
parent 14efe072fa
commit dd4e184600
16 changed files with 1041 additions and 331 deletions

View File

@@ -64,9 +64,6 @@ public class NotificationManager : Object {
_badge.set_visible(_badge_visible);
_badge.set_halign(Gtk.Align.START);
// Connect badge changed signal
_badge.changed.connect(_on_badge_changed);
// Set up tray icon
_tray_icon = Gtk.TrayIcon.new();
_tray_icon.set_icon_name("rssuper");
@@ -75,26 +72,8 @@ public class NotificationManager : Object {
// Connect tray icon clicked signal
_tray_icon.clicked.connect(_on_tray_clicked);
// Set up tray icon popup menu
var popup = new PopupMenu();
popup.add_item(new Gtk.Label("Notifications: " + _unread_count.toString()));
popup.add_item(new Gtk.Separator());
popup.add_item(new Gtk.Label("Mark all as read"));
popup.add_item(new Gtk.Separator());
popup.add_item(new Gtk.Label("Settings"));
popup.add_item(new Gtk.Label("Exit"));
popup.connect_closed(_on_tray_closed);
_tray_icon.set_popup(popup);
// Connect tray icon popup menu signal
popup.menu_closed.connect(_on_tray_popup_closed);
// Set up tray icon popup handler
_tray_icon.set_popup_handler(_on_tray_popup);
// Set up tray icon tooltip
_tray_icon.set_tooltip_text("RSSuper - Press for notifications");
}
/**
@@ -103,13 +82,10 @@ public class NotificationManager : Object {
public void set_up_badge() {
_badge.set_visible(_badge_visible);
_badge.set_halign(Gtk.Align.START);
// Set up badge changed signal
_badge.changed.connect(_on_badge_changed);
}
/**
* Set up the tray icon
/**
* Set up the tray icon popup menu
*/
public void set_up_tray_icon() {
_tray_icon.set_icon_name("rssuper");
@@ -118,25 +94,8 @@ public class NotificationManager : Object {
// Connect tray icon clicked signal
_tray_icon.clicked.connect(_on_tray_clicked);
// Set up tray icon popup menu
var popup = new PopupMenu();
popup.add_item(new Gtk.Label("Notifications: " + _unread_count.toString()));
popup.add_item(new Gtk.Separator());
popup.add_item(new Gtk.Label("Mark all as read"));
popup.add_item(new Gtk.Separator());
popup.add_item(new Gtk.Label("Settings"));
popup.add_item(new Gtk.Label("Exit"));
popup.connect_closed(_on_tray_closed);
_tray_icon.set_popup(popup);
// Connect tray icon popup menu signal
popup.menu_closed.connect(_on_tray_popup_closed);
// Set up tray icon popup handler
_tray_icon.set_popup_handler(_on_tray_popup);
// Set up tray icon tooltip
_tray_icon.set_tooltip_text("RSSuper - Press for notifications");
}
@@ -144,7 +103,7 @@ public class NotificationManager : Object {
* Show badge
*/
public void show_badge() {
_badge.set_visible(_badge_visible);
_badge.set_visible(true);
}
/**
@@ -158,7 +117,7 @@ public class NotificationManager : Object {
* Show badge with count
*/
public void show_badge_with_count(int count) {
_badge.set_visible(_badge_visible);
_badge.set_visible(true);
_badge.set_label(count.toString());
}
@@ -173,14 +132,6 @@ public class NotificationManager : Object {
_badge.set_label(count.toString());
}
// Update tray icon popup
if (_tray_icon != null) {
var popup = _tray_icon.get_popup();
if (popup != null) {
popup.set_label("Notifications: " + count.toString());
}
}
// Show badge if count > 0
if (count > 0) {
show_badge();
@@ -193,14 +144,6 @@ public class NotificationManager : Object {
public void clear_unread_count() {
_unread_count = 0;
hide_badge();
// Update tray icon popup
if (_tray_icon != null) {
var popup = _tray_icon.get_popup();
if (popup != null) {
popup.set_label("Notifications: 0");
}
}
}
/**
@@ -300,15 +243,7 @@ public class NotificationManager : Object {
show_with_badge(title, body, icon, Urgency.NORMAL);
}
/**
* Handle badge changed signal
*/
private void _on_badge_changed(Gtk.Badge badge) {
var count = badge.get_label();
if (!string.IsNullOrEmpty(count)) {
_unread_count = int.Parse(count);
}
}
/**
* Handle tray icon clicked signal
@@ -317,35 +252,7 @@ public class NotificationManager : Object {
show_notifications_panel();
}
/**
* Handle tray icon popup closed signal
*/
private void _on_tray_popup_closed(Gtk.Popup popup) {
// Popup closed, hide icon
if (_tray_icon != null) {
_tray_icon.hide();
}
}
/**
* Handle tray icon popup open signal
*/
private void _on_tray_popup(Gtk.TrayIcon tray, Gtk.MenuItem menu) {
// Show icon when popup is opened
if (_tray_icon != null) {
_tray_icon.show();
}
}
/**
* Handle tray icon closed signal
*/
private void _on_tray_closed(Gtk.App app) {
// App closed, hide tray icon
if (_tray_icon != null) {
_tray_icon.hide();
}
}
/**
* Show notifications panel

View File

@@ -64,32 +64,25 @@ public class NotificationService : Object {
*
* @param title The notification title
* @param body The notification body
* @param icon Optional icon path
* @param urgency Urgency level (NORMAL, CRITICAL, LOW)
* @param timestamp Optional timestamp (defaults to now)
* @return Notification instance
*/
public Notification create(string title, string body,
string? icon = null,
Urgency urgency = Urgency.NORMAL,
DateTime timestamp = null) {
DateTime? timestamp = null) {
_notification = Gio.Notification.new(_default_title);
_notification.set_body(body);
_notification.set_urgency(urgency);
if (timestamp == null) {
_notification.set_time_now();
} else {
_notification.set_time(timestamp);
if (string.IsNullOrEmpty(title)) {
warning("Notification title cannot be empty");
title = _default_title;
}
return _notification;
}
/**
* Create a notification with summary and icon
*/
public Notification create(string title, string body, string icon,
Urgency urgency = Urgency.NORMAL,
DateTime timestamp = null) {
if (string.IsNullOrEmpty(body)) {
warning("Notification body cannot be empty");
body = "";
}
_notification = Gio.Notification.new(title);
_notification.set_body(body);
@@ -101,38 +94,12 @@ public class NotificationService : Object {
_notification.set_time(timestamp);
}
// Set icon
try {
_notification.set_icon(icon);
} catch (Error e) {
warning("Failed to set icon: %s", e.message);
}
return _notification;
}
/**
* Create a notification with summary, body, and icon
*/
public Notification create(string summary, string body, string icon,
Urgency urgency = Urgency.NORMAL,
DateTime timestamp = null) {
_notification = Gio.Notification.new(summary);
_notification.set_body(body);
_notification.set_urgency(urgency);
if (timestamp == null) {
_notification.set_time_now();
} else {
_notification.set_time(timestamp);
}
// Set icon
try {
_notification.set_icon(icon);
} catch (Error e) {
warning("Failed to set icon: %s", e.message);
if (icon != null) {
try {
_notification.set_icon(icon);
} catch (Error e) {
warning("Failed to set icon: %s", e.message);
}
}
return _notification;

View File

@@ -0,0 +1,218 @@
/*
* sync-scheduler-tests.vala
*
* Unit tests for SyncScheduler
*/
using GLib;
namespace RSSuper {
public class SyncSchedulerTests : TestCase {
private SyncScheduler? _scheduler;
protected void setup() {
_scheduler = SyncScheduler.get_instance();
_scheduler.reset_sync_schedule();
}
protected void teardown() {
_scheduler = null;
}
public void test_initial_state() {
// Test initial state
assert(_scheduler.get_last_sync_date() == null, "Last sync date should be null initially");
assert(_scheduler.get_preferred_sync_interval_hours() == 6,
"Default sync interval should be 6 hours");
assert(_scheduler.is_sync_due(), "Sync should be due initially");
}
public void test_update_sync_interval_few_feeds() {
// Test with few feeds (high frequency)
_scheduler.update_sync_interval(5, UserActivityLevel.HIGH);
assert(_scheduler.get_preferred_sync_interval_hours() <= 2,
"Sync interval should be reduced for few feeds with high activity");
}
public void test_update_sync_interval_many_feeds() {
// Test with many feeds (lower frequency)
_scheduler.update_sync_interval(500, UserActivityLevel.LOW);
assert(_scheduler.get_preferred_sync_interval_hours() >= 24,
"Sync interval should be increased for many feeds with low activity");
}
public void test_update_sync_interval_clamps_to_max() {
// Test that interval is clamped to maximum
_scheduler.update_sync_interval(1000, UserActivityLevel.LOW);
assert(_scheduler.get_preferred_sync_interval_hours() <= 24,
"Sync interval should not exceed maximum (24 hours)");
}
public void test_is_sync_due_after_update() {
// Simulate a sync by setting last sync timestamp
_scheduler.set_last_sync_timestamp();
assert(!_scheduler.is_sync_due(), "Sync should not be due immediately after sync");
}
public void test_reset_sync_schedule() {
// Set some state
_scheduler.set_preferred_sync_interval_hours(12);
_scheduler.set_last_sync_timestamp();
// Reset
_scheduler.reset_sync_schedule();
// Verify reset
assert(_scheduler.get_last_sync_date() == null,
"Last sync date should be null after reset");
assert(_scheduler.get_preferred_sync_interval_hours() == 6,
"Sync interval should be reset to default (6 hours)");
}
public void test_user_activity_level_high() {
var activity_level = UserActivityLevel.calculate(10, 60);
assert(activity_level == UserActivityLevel.HIGH,
"Should be HIGH activity");
}
public void test_user_activity_level_medium() {
var activity_level = UserActivityLevel.calculate(3, 3600);
assert(activity_level == UserActivityLevel.MEDIUM,
"Should be MEDIUM activity");
}
public void test_user_activity_level_low() {
var activity_level = UserActivityLevel.calculate(0, 86400 * 7);
assert(activity_level == UserActivityLevel.LOW,
"Should be LOW activity");
}
public void test_schedule_next_sync() {
// Schedule should succeed
var result = _scheduler.schedule_next_sync();
assert(result, "Schedule next sync should succeed");
}
public void test_cancel_sync_timeout() {
// Schedule then cancel
_scheduler.schedule_next_sync();
_scheduler.cancel_sync_timeout();
// Should not throw
assert(true, "Cancel should not throw");
}
}
public class SyncWorkerTests : TestCase {
private SyncWorker? _worker;
protected void setup() {
_worker = new SyncWorker();
}
protected void teardown() {
_worker = null;
}
public void test_perform_sync_empty() {
// Sync with no subscriptions should succeed
var result = _worker.perform_sync();
assert(result.feeds_synced == 0, "Should sync 0 feeds");
assert(result.articles_fetched == 0, "Should fetch 0 articles");
}
public void test_sync_result() {
var errors = new List<Error>();
var result = new SyncResult(5, 100, errors);
assert(result.feeds_synced == 5, "Should have 5 feeds synced");
assert(result.articles_fetched == 100, "Should have 100 articles fetched");
}
public void test_subscription() {
var sub = new Subscription("test-id", "Test Feed", "http://example.com/feed");
assert(sub.id == "test-id", "ID should match");
assert(sub.title == "Test Feed", "Title should match");
assert(sub.url == "http://example.com/feed", "URL should match");
}
public void test_feed_data() {
var articles = new List<Article>();
articles.append(new Article("art-1", "Article 1", "http://example.com/1"));
var feed_data = new FeedData("Test Feed", articles);
assert(feed_data.title == "Test Feed", "Title should match");
assert(feed_data.articles.length() == 1, "Should have 1 article");
}
public void test_article() {
var article = new Article("art-1", "Article 1", "http://example.com/1", 1234567890);
assert(article.id == "art-1", "ID should match");
assert(article.title == "Article 1", "Title should match");
assert(article.link == "http://example.com/1", "Link should match");
assert(article.published == 1234567890, "Published timestamp should match");
}
}
public class BackgroundSyncServiceTests : TestCase {
private BackgroundSyncService? _service;
protected void setup() {
_service = BackgroundSyncService.get_instance();
}
protected void teardown() {
_service.shutdown();
_service = null;
}
public void test_singleton() {
var instance1 = BackgroundSyncService.get_instance();
var instance2 = BackgroundSyncService.get_instance();
assert(instance1 == instance2, "Should return same instance");
}
public void test_is_syncing_initially_false() {
assert(!_service.is_syncing(), "Should not be syncing initially");
}
public void test_schedule_next_sync() {
var result = _service.schedule_next_sync();
assert(result, "Schedule should succeed");
}
public void test_cancel_all_pending() {
_service.schedule_next_sync();
_service.cancel_all_pending();
// Should not throw
assert(true, "Cancel should not throw");
}
}
} // namespace RSSuper
// Main test runner
public static int main(string[] args) {
Test.init(ref args);
// Add test suites
Test.add_suite("SyncScheduler", SyncSchedulerTests.new);
Test.add_suite("SyncWorker", SyncWorkerTests.new);
Test.add_suite("BackgroundSyncService", BackgroundSyncServiceTests.new);
return Test.run();
}