feat: notifications

This commit is contained in:
relikd
2025-10-25 11:32:38 +02:00
parent def174c65f
commit 0a23819428
21 changed files with 372 additions and 21 deletions

View File

@@ -7,6 +7,7 @@
#import "StoreCoordinator.h"
#import "SettingsFeeds+DragDrop.h"
#import "URLScheme.h"
#import "NotifyEndpoint.h"
#import "NSURL+Ext.h"
#import "NSError+Ext.h"
@@ -45,6 +46,10 @@
// mostly for version migration 0.9.4 ~> 1.0 (favicon storage)
if (initial) [UpdateScheduler updateAllFavicons];
}
// Notifications are disabled by default so this wont trigger for first app launch.
// Also, this will register the notification delegate and respond to click & open feed.
[NotifyEndpoint activate];
}
- (void)applicationWillTerminate:(NSNotification *)aNotification {

View File

@@ -10,6 +10,7 @@ NS_ASSUME_NONNULL_BEGIN
// Generator methods / Feed update
+ (instancetype)newFeedAndMetaInContext:(NSManagedObjectContext*)context;
- (NSString*)notificationID;
- (void)updateWithRSS:(RSParsedFeed*)obj postUnreadCountChange:(BOOL)flag;
- (NSMenuItem*)newMenuItem;
// Getter & Setter
@@ -17,6 +18,7 @@ NS_ASSUME_NONNULL_BEGIN
- (void)setNewIcon:(NSURL*)location;
// Article properties
- (nullable NSArray<FeedArticle*>*)sortedArticles;
- (NSUInteger)countUnread;
@end
NS_ASSUME_NONNULL_END

View File

@@ -6,6 +6,7 @@
#import "FeedGroup+Ext.h"
#import "FeedArticle+Ext.h"
#import "StoreCoordinator.h"
#import "NotifyEndpoint.h"
#import "NSURL+Ext.h"
@implementation Feed (Ext)
@@ -17,6 +18,11 @@
return feed;
}
/// unique ID used for notifications. returns @c objectID.URIRepresentation.absoluteString
- (NSString*)notificationID {
return self.objectID.URIRepresentation.absoluteString;
}
/// Call @c indexPathString on @c .group and update @c .indexPath if current value is different.
- (void)calculateAndSetIndexPathString {
NSString *pthStr = [self.group indexPathString];
@@ -110,10 +116,12 @@
- (NSUInteger)deleteArticles:(NSMutableSet<FeedArticle*>*)localSet withRemoteSet:(NSArray<RSParsedArticle*>*)remoteSet {
NSUInteger c = 0;
NSMutableSet<FeedArticle*> *deletingSet = [NSMutableSet setWithCapacity:localSet.count];
NSMutableArray *dismissed = [NSMutableArray array];
for (FeedArticle *fa in localSet) {
if (![self findLocalArticle:fa inRemoteSet:remoteSet]) {
if (fa.unread) ++c;
// TODO: keep unread articles?
[dismissed addObject:fa.notificationID];
[self.managedObjectContext deleteObject:fa];
[deletingSet addObject:fa];
}
@@ -121,6 +129,7 @@
if (deletingSet.count > 0) {
[localSet minusSet:deletingSet];
[self removeArticles:deletingSet];
[NotifyEndpoint dismiss:dismissed];
}
return c;
}
@@ -176,6 +185,15 @@
return nil;
}
/// Number of unread articles
- (NSUInteger)countUnread {
NSUInteger count = 0;
for (FeedArticle *article in self.articles) {
count += article.unread;
}
return count;
}
#pragma mark - Icon -

View File

@@ -6,6 +6,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface FeedArticle (Ext)
+ (instancetype)newArticle:(RSParsedArticle*)entry inContext:(NSManagedObjectContext*)moc;
- (NSString*)notificationID;
- (void)updateArticleIfChanged:(RSParsedArticle*)entry;
- (NSMenuItem*)newMenuItem;
@end

View File

@@ -1,8 +1,10 @@
@import RSXML2.RSParsedArticle;
#import "FeedArticle+Ext.h"
#import "Feed+Ext.h"
#import "Constants.h"
#import "UserPrefs.h"
#import "StoreCoordinator.h"
#import "NotifyEndpoint.h"
#import "NSString+Ext.h"
@implementation FeedArticle (Ext)
@@ -25,6 +27,11 @@
return fa;
}
/// unique ID used for notifications. returns @c objectID.URIRepresentation.absoluteString
- (NSString*)notificationID {
return self.objectID.URIRepresentation.absoluteString;
}
- (void)updateArticleIfChanged:(RSParsedArticle*)entry {
[self setGuidIfChanged:entry.guid];
[self setTitleIfChanged:entry.title];
@@ -77,6 +84,8 @@
[StoreCoordinator saveContext:moc andParent:YES];
NSNumber *num = (fa.unread ? @+1 : @-1);
PostNotification(kNotificationTotalUnreadCountChanged, num);
[NotifyEndpoint dismiss:fa.feed.countUnread > 0 ? @[fa.notificationID] : @[fa.notificationID, fa.feed.notificationID]];
}
[moc reset];
}

View File

@@ -30,7 +30,7 @@ NS_ASSUME_NONNULL_BEGIN
// Unread articles list & mark articled read
+ (NSArray<FeedArticle*>*)articlesAtPath:(nullable NSString*)path isFeed:(BOOL)feedFlag sorted:(BOOL)sortFlag unread:(BOOL)readFlag inContext:(NSManagedObjectContext*)moc limit:(NSUInteger)limit;
+ (void)updateArticles:(NSArray<FeedArticle*>*)list markRead:(BOOL)markRead andOpen:(BOOL)openLinks inContext:(NSManagedObjectContext*)moc;
+ (NSArray<NSString*>*)updateArticles:(NSArray<FeedArticle*>*)list markRead:(BOOL)markRead andOpen:(BOOL)openLinks inContext:(NSManagedObjectContext*)moc;
// Restore sound state
+ (void)cleanupAndShowAlert:(BOOL)flag;

View File

@@ -4,6 +4,7 @@
#import "FaviconDownload.h"
#import "UserPrefs.h"
#import "Feed+Ext.h"
#import "FeedArticle+Ext.h"
#import "NSURL+Ext.h"
#import "NSError+Ext.h"
#import "NSFetchRequest+Ext.h"
@@ -210,28 +211,40 @@
@param list Should only contain @c FeedArticle
@param markRead Whether the articles should be marked read or unread.
@param openLinks Whether to open the link or mark read without opening
@return @c notificationID for all articles that were opened (empty if @c openLinks=NO or open failed).
*/
+ (void)updateArticles:(NSArray<FeedArticle*>*)list markRead:(BOOL)markRead andOpen:(BOOL)openLinks inContext:(NSManagedObjectContext*)moc {
BOOL success = NO;
+ (NSArray<NSString*>*)updateArticles:(NSArray<FeedArticle*>*)list markRead:(BOOL)markRead andOpen:(BOOL)openLinks inContext:(NSManagedObjectContext*)moc {
if (openLinks) {
NSMutableArray<NSURL*> *urls = [NSMutableArray arrayWithCapacity:list.count];
for (FeedArticle *fa in list) {
if (fa.link.length > 0)
[urls addObject:[NSURL URLWithString:fa.link]];
}
if (urls.count > 0)
success = UserPrefsOpenURLs(urls);
if (urls.count > 0 && !UserPrefsOpenURLs(urls))
return nil; // if success == NO, do not modify unread state & exit
}
// if success == NO, do not modify unread state
if (!openLinks || success) {
for (FeedArticle *fa in list) {
for (FeedArticle *fa in list) {
if (fa.unread == markRead) { // only if differs
fa.unread = !markRead;
}
[self saveContext:moc andParent:YES];
[moc reset];
NSNumber *num = [NSNumber numberWithInteger: (markRead ? -1 : +1) * (NSInteger)list.count ];
PostNotification(kNotificationTotalUnreadCountChanged, num);
}
[self saveContext:moc andParent:YES];
// gather uri-ids for notification dismiss
NSMutableArray<NSString*> *dbRefs = [NSMutableArray array];
if (markRead) {
for (FeedArticle *fa in list) {
[dbRefs addObject:fa.notificationID];
[dbRefs addObject:fa.feed.notificationID];
}
}
[moc reset];
NSNumber *num = [NSNumber numberWithInteger: (markRead ? -1 : +1) * (NSInteger)list.count ];
PostNotification(kNotificationTotalUnreadCountChanged, num);
return dbRefs;
}

View File

@@ -17,7 +17,7 @@ NS_ASSUME_NONNULL_BEGIN
// Scheduling
+ (void)scheduleNextFeed;
+ (void)forceUpdateAllFeeds;
+ (void)downloadList:(NSArray<Feed*>*)list userInitiated:(BOOL)flag finally:(nullable os_block_t)block;
+ (void)downloadList:(NSArray<Feed*>*)list userInitiated:(BOOL)flag notifications:(BOOL)notify finally:(nullable os_block_t)block;
+ (void)updateAllFavicons;
// Auto Download & Parse Feed URL
+ (void)autoDownloadAndParseURL:(NSString*)url;

View File

@@ -2,11 +2,13 @@
#import "UpdateScheduler.h"
#import "Constants.h"
#import "StoreCoordinator.h"
#import "NotifyEndpoint.h"
#import "NSDate+Ext.h"
#import "FeedDownload.h"
#import "FaviconDownload.h"
#import "Feed+Ext.h"
#import "FeedArticle+Ext.h"
#import "FeedMeta+Ext.h"
#import "FeedGroup+Ext.h"
@@ -129,7 +131,7 @@ static _Atomic(NSUInteger) _queueSize = 0;
NSArray<Feed*> *list = [StoreCoordinator listOfFeedsThatNeedUpdate:updateAll inContext:moc];
//NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early.");
[self downloadList:list userInitiated:updateAll finally:^{
[self downloadList:list userInitiated:updateAll notifications:YES finally:^{
[StoreCoordinator saveContext:moc andParent:YES]; // save parents too ...
[moc reset];
[self scheduleNextFeed]; // always reset the timer
@@ -147,7 +149,7 @@ static _Atomic(NSUInteger) _queueSize = 0;
}
/// Download list of feeds. Either silently in background or with alerts in foreground.
+ (void)downloadList:(NSArray<Feed*>*)list userInitiated:(BOOL)flag finally:(nullable os_block_t)block {
+ (void)downloadList:(NSArray<Feed*>*)list userInitiated:(BOOL)flag notifications:(BOOL)notify finally:(nullable os_block_t)block {
if (![self allowNetworkConnection]) {
if (block) block();
return;
@@ -158,7 +160,7 @@ static _Atomic(NSUInteger) _queueSize = 0;
dispatch_group_t group = dispatch_group_create();
for (Feed *f in list) {
dispatch_group_enter(group);
[self updateFeed:f alert:flag isForced:flag finally:^{
[self updateFeed:f alert:flag isForced:flag notifications:notify finally:^{
atomic_fetch_sub_explicit(&_queueSize, 1, memory_order_relaxed);
PostNotification(kNotificationBackgroundUpdateInProgress, @(_queueSize));
dispatch_group_leave(group);
@@ -178,7 +180,7 @@ static inline void AlertDownloadError(NSError *err, NSString *url) {
Start download request with existing @c Feed object. Reuses etag and modified headers (unless articles count is 0).
@note Will post a @c kNotificationArticlesUpdated notification if download was successful and status code is @b not 304.
*/
+ (void)updateFeed:(Feed*)feed alert:(BOOL)alert isForced:(BOOL)forced finally:(nullable os_block_t)block {
+ (void)updateFeed:(Feed*)feed alert:(BOOL)alert isForced:(BOOL)forced notifications:(BOOL)notify finally:(nullable os_block_t)block {
NSManagedObjectContext *moc = feed.managedObjectContext;
NSManagedObjectID *oid = feed.objectID;
[[FeedDownload withFeed:feed forced:forced] startWithBlock:^(FeedDownload *mem) {
@@ -188,7 +190,37 @@ static inline void AlertDownloadError(NSError *err, NSString *url) {
BOOL recentlyAdded = (f.articles.count == 0); // before copy values
BOOL downloadIcon = (!f.hasIcon && (recentlyAdded || forced));
BOOL needsNotification = [mem copyValuesTo:f ignoreError:NO];
// need to gather object before save, because afterwards list will be empty
NSArray *inserted = notify ? moc.insertedObjects.allObjects : nil;
NSArray *deleted = moc.deletedObjects.allObjects;
[StoreCoordinator saveContext:moc andParent:YES];
// after save, update notifications
// dismiss previously delivered notifications
if (deleted) {
NSMutableArray *ids = [NSMutableArray array];
for (FeedArticle *article in deleted) { // will contain non-articles too
if ([article isKindOfClass:[FeedArticle class]] || [article isKindOfClass:[Feed class]]) {
[ids addObject:article.notificationID];
}
}
[NotifyEndpoint dismiss:ids]; // no-op if empty
}
// post new notification (if needed)
if (notify && inserted) {
BOOL didAddAny = NO;
for (FeedArticle *article in inserted) { // will contain non-articles too
if ([article isKindOfClass:[FeedArticle class]]) {
[NotifyEndpoint postArticle:article];
didAddAny = YES;
}
}
if (didAddAny)
[NotifyEndpoint postFeed:f];
}
if (needsNotification)
PostNotification(kNotificationArticlesUpdated, oid);
if (downloadIcon && !mem.error) {

View File

@@ -13,6 +13,7 @@
/** default: @c nil */ static NSString* const Pref_modalSheetWidth = @"modalSheetWidth";
// ------ General settings ------ (Preferences > General Tab) ------
/** default: @c nil */ static NSString* const Pref_defaultHttpApplication = @"defaultHttpApplication";
/** default: @c nil */ static NSString* const Pref_notificationType = @"notificationType";
// ------ Appearance matrix ------ (Preferences > Appearance Tab) ------
/** default: @c YES */ static NSString* const Pref_globalTintMenuIcon = @"globalTintMenuBarIcon";
/** default: @c YES */ static NSString* const Pref_globalUpdateAll = @"globalUpdateAll";
@@ -49,6 +50,16 @@
void UserPrefsInit(void);
NSColor* UserPrefsColor(NSString *key, NSColor *defaultColor); // Change with: defaults write de.relikd.baRSS {KEY} -string "#FBA33A"
typedef NS_ENUM(NSInteger, NotificationType) {
NotificationTypeDisabled,
NotificationTypePerArticle,
NotificationTypePerFeed,
NotificationTypeGlobal,
};
NotificationType UserPrefsNotificationType(void);
NSString* NotificationTypeToString(NotificationType typ);
// ------ Getter ------
/// Helper method calls @c (standardUserDefaults)boolForKey:
static inline BOOL UserPrefsBool(NSString* const key) { return [[NSUserDefaults standardUserDefaults] boolForKey:key]; }

View File

@@ -44,3 +44,22 @@ NSColor* UserPrefsColor(NSString *key, NSColor *defaultColor) {
}
return defaultColor;
}
/// Convert stored notification type string into enum
NotificationType UserPrefsNotificationType(void) {
NSString *typ = UserPrefsString(Pref_notificationType);
if ([typ isEqualToString:@"article"]) return NotificationTypePerArticle;
if ([typ isEqualToString:@"feed"]) return NotificationTypePerFeed;
if ([typ isEqualToString:@"global"]) return NotificationTypeGlobal;
return NotificationTypeDisabled;
}
/// Convert enum type to storable string
NSString* NotificationTypeToString(NotificationType typ) {
switch (typ) {
case NotificationTypePerArticle: return @"article";
case NotificationTypePerFeed: return @"feed";
case NotificationTypeGlobal: return @"global";
default: return nil;
}
}

View File

@@ -0,0 +1,18 @@
@import Cocoa;
@import UserNotifications;
@class Feed, FeedArticle;
NS_ASSUME_NONNULL_BEGIN
@interface NotifyEndpoint : NSObject <UNUserNotificationCenterDelegate>
+ (void)activate;
+ (void)setGlobalCount:(NSUInteger)count;
+ (void)postFeed:(Feed*)feed;
+ (void)postArticle:(FeedArticle*)article;
+ (void)dismiss:(nullable NSArray<NSString*>*)list;
@end
NS_ASSUME_NONNULL_END

View File

@@ -0,0 +1,171 @@
#import "NotifyEndpoint.h"
#import "UserPrefs.h"
#import "StoreCoordinator.h"
#import "Feed+Ext.h"
#import "FeedArticle+Ext.h"
/**
Sent for global unread count notification alert (Notification Center)
*/
static NSString* const kNotifyIdGlobal = @"global";
@implementation NotifyEndpoint
static NotifyEndpoint *singleton = nil;
static NotificationType notifyType;
/// Ask user for permission to send notifications @b AND register delegate to respond to alert banner clicks.
/// @note Called every time user changes notification settings
+ (void)activate {
notifyType = UserPrefsNotificationType();
// even if disabled, register delegate. This allows to open previously sent notifications
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
singleton = [NotifyEndpoint new];
UNUserNotificationCenter.currentNotificationCenter.delegate = singleton;
});
if (notifyType == NotificationTypeDisabled) {
return;
}
[UNUserNotificationCenter.currentNotificationCenter requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionSound completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
NSAlert *alert = [[NSAlert alloc] init];
alert.messageText = NSLocalizedString(@"Notifications Disabled", nil);
alert.informativeText = NSLocalizedString(@"Either enable notifications in System Settings, or disable notifications in baRSS settings.", nil);
alert.alertStyle = NSAlertStyleInformational;
[alert runModal];
});
}
}];
}
/// Set (or update) global "X unread articles"
+ (void)setGlobalCount:(NSUInteger)count {
if (notifyType != NotificationTypeGlobal) {
return;
}
if (count > 0) {
// TODO: how to handle global count updates?
// ignore and keep old count until 0?
// or update count and show a new notification banner?
[self send:kNotifyIdGlobal
title:nil
body:[NSString stringWithFormat:@"%ld unread articles", count]];
} else {
[self dismiss:@[kNotifyIdGlobal]];
}
}
/// Triggers feed notifications (if enabled)
+ (void)postFeed:(Feed*)feed {
if (notifyType != NotificationTypePerFeed) {
return;
}
NSUInteger count = feed.countUnread;
if (count > 0) {
[feed.managedObjectContext obtainPermanentIDsForObjects:@[feed] error:nil];
[self send:feed.notificationID
title:feed.title
body:[NSString stringWithFormat:@"%ld unread articles", count]];
}
}
/// Triggers article notifications (if enabled)
+ (void)postArticle:(FeedArticle*)article {
if (notifyType != NotificationTypePerArticle) {
return;
}
[article.managedObjectContext obtainPermanentIDsForObjects:@[article] error:nil];
[self send:article.notificationID
title:article.title
body:article.abstract ? article.abstract : article.body];
}
/// Close already posted notifications because they were opened via menu
+ (void)dismiss:(nullable NSArray<NSString*>*)list {
if (list.count > 0) {
[UNUserNotificationCenter.currentNotificationCenter removeDeliveredNotificationsWithIdentifiers:list];
}
}
#pragma mark - Helper methods
/// Post notification (immediatelly).
/// @param identifier Used to identify a specific instance (and dismiss a previously shown notification).
+ (void)send:(NSString *)identifier title:(nullable NSString *)title body:(nullable NSString *)body {
UNMutableNotificationContent *msg = [UNMutableNotificationContent new];
msg.title = title;
msg.body = body;
// common settings:
// TODO: make sound configurable?
msg.sound = [UNNotificationSound defaultSound];
[self send:identifier content: msg];
}
/// Internal method for queueing a new notification.
+ (void)send:(NSString *)identifier content:(UNMutableNotificationContent*)msg {
UNUserNotificationCenter *center = UNUserNotificationCenter.currentNotificationCenter;
[center getNotificationSettingsWithCompletionHandler:^(UNNotificationSettings * _Nonnull settings) {
if (settings.authorizationStatus != UNAuthorizationStatusAuthorized) {
return;
}
UNNotificationRequest *req = [UNNotificationRequest requestWithIdentifier:identifier content:msg trigger:nil];
[center addNotificationRequest:req withCompletionHandler:^(NSError * _Nullable error) {
if (error) {
NSLog(@"Could not send notification: %@", error);
}
}];
}];
}
#pragma mark - Delegate
/// Must be implemented to show notifications while the app is in foreground
- (void)userNotificationCenter:(UNUserNotificationCenter *)center willPresentNotification:(UNNotification *)notification withCompletionHandler:(void (^)(UNNotificationPresentationOptions))completionHandler {
// all the options
UNNotificationPresentationOptions common = UNNotificationPresentationOptionSound | UNNotificationPresentationOptionBadge;
if (@available(macOS 11.0, *)) {
completionHandler(common | UNNotificationPresentationOptionBanner | UNNotificationPresentationOptionList);
} else {
completionHandler(common | UNNotificationPresentationOptionAlert);
}
}
/// Callback method when user clicks on alert banner
- (void)userNotificationCenter:(UNUserNotificationCenter *)center didReceiveNotificationResponse:(UNNotificationResponse *)response withCompletionHandler:(void (^)(void))completionHandler {
NSArray<FeedArticle*> *articles;
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
NSString *theId = response.notification.request.identifier;
if ([theId isEqualToString:kNotifyIdGlobal]) {
// global notification
articles = [StoreCoordinator articlesAtPath:nil isFeed:NO sorted:YES unread:YES inContext:moc limit:0];
} else {
NSURL *uri = [NSURL URLWithString:theId];
NSManagedObjectID *oid = [moc.persistentStoreCoordinator managedObjectIDForURIRepresentation:uri];
NSManagedObject *obj = [moc objectWithID:oid];
if ([obj isKindOfClass:[FeedArticle class]]) {
// per-article notification
articles = @[(FeedArticle*)obj];
} else if ([obj isKindOfClass:[Feed class]]) {
// per-feed notification
articles = [[[(Feed*)obj articles]
filteredSetUsingPredicate:[NSPredicate predicateWithFormat:@"unread = 1"]]
sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:NO]]];
} else {
return;
}
}
[StoreCoordinator updateArticles:articles markRead:YES andOpen:YES inContext:moc];
}
@end

View File

@@ -138,7 +138,7 @@ const NSPasteboardType dragReorder = @"de.relikd.baRSS.drag-reorder";
if (selection.count > 0)
[self.dataStore setSelectionIndexPaths:[selection sortedArrayUsingSelector:@selector(compare:)]];
[UpdateScheduler downloadList:feedsList userInitiated:YES finally:^{
[UpdateScheduler downloadList:feedsList userInitiated:YES notifications:NO finally:^{
[self endCoreDataChangeUndoEmpty:NO forceUndo:NO];
for (Feed *f in feedsList)
[moc refreshObject:f.group mergeChanges:NO]; // fixes blank icon if imported with no inet conn

View File

@@ -5,6 +5,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface SettingsGeneral : NSViewController
- (void)clickHowToDefaults:(NSButton *)sender;
- (void)changeHttpApplication:(NSPopUpButton *)sender;
- (void)changeNotificationType:(NSPopUpButton *)sender;
@end
NS_ASSUME_NONNULL_END

View File

@@ -3,6 +3,7 @@
#import "StoreCoordinator.h"
#import "Constants.h"
#import "SettingsGeneralView.h"
#import "NotifyEndpoint.h"
@interface SettingsGeneral()
@property (strong) IBOutlet SettingsGeneralView *view; // override
@@ -28,6 +29,23 @@
defaultApp.lastItem.representedObject = bundleID;
}
[defaultApp selectItemAtIndex:[defaultApp indexOfItemWithRepresentedObject:UserPrefsString(Pref_defaultHttpApplication)]];
// Notification settings (disabled, per article, per feed, total)
NSPopUpButton *notify = self.view.popupNotificationType;
[notify removeAllItems];
[notify addItemWithTitle:NSLocalizedString(@"Disabled", @"Disable notifications")];
notify.lastItem.representedObject = @"";
[notify addItemsWithTitles:@[
NSLocalizedString(@"Disabled", @"Disable notifications"),
NSLocalizedString(@"Article title (per article)", @"Show article title in notification"),
NSLocalizedString(@"Number of articles (per feed)", @"Show “feed X: N new articles”"),
NSLocalizedString(@"Generic “new articles” (over all)", @"Show total “N new article”"),
]];
notify.itemArray[0].representedObject = NotificationTypeToString(NotificationTypeDisabled);
notify.itemArray[1].representedObject = NotificationTypeToString(NotificationTypePerArticle);
notify.itemArray[2].representedObject = NotificationTypeToString(NotificationTypePerFeed);
notify.itemArray[3].representedObject = NotificationTypeToString(NotificationTypeGlobal);
[notify selectItemAtIndex:[notify indexOfItemWithRepresentedObject:NotificationTypeToString(UserPrefsNotificationType())]];
}
/// Get human readable application name such as 'Safari' or 'baRSS'
@@ -65,4 +83,9 @@
UserPrefsSet(Pref_defaultHttpApplication, sender.selectedItem.representedObject);
}
- (void)changeNotificationType:(NSPopUpButton *)sender {
UserPrefsSet(Pref_notificationType, sender.selectedItem.representedObject);
[NotifyEndpoint activate];
}
@end

View File

@@ -6,6 +6,7 @@ NS_ASSUME_NONNULL_BEGIN
@interface SettingsGeneralView : NSView
@property (strong) IBOutlet NSTextField *defaultReader;
@property (strong) IBOutlet NSPopUpButton* popupHttpApplication;
@property (strong) IBOutlet NSPopUpButton* popupNotificationType;
- (instancetype)initWithController:(SettingsGeneral*)controller NS_DESIGNATED_INITIALIZER;
- (instancetype)initWithFrame:(NSRect)frameRect NS_UNAVAILABLE;

View File

@@ -6,15 +6,23 @@
- (instancetype)initWithController:(SettingsGeneral*)controller {
self = [super initWithFrame:NSMakeRect(0, 0, 320, 327)];
// Change default feed reader application
NSTextField *l1 = [[NSView label:NSLocalizedString(@"Default feed reader:", nil)] placeIn:self x:PAD_WIN yTop:PAD_WIN + 3];
NSButton *help = [[[NSView helpButton] action:@selector(clickHowToDefaults:) target:controller] placeIn:self xRight:PAD_WIN yTop:PAD_WIN];
self.defaultReader = [[[[NSView label:@""] bold] placeIn:self x:NSMaxX(l1.frame) + PAD_S yTop:PAD_WIN + 3] sizeToRight:NSWidth(help.frame) + PAD_WIN];
// Popup button 'Open URLs with:'
CGFloat y = YFromTop(help) + PAD_M;
NSTextField *l2 = [[NSView label:NSLocalizedString(@"Open URLs with:", nil)] placeIn:self x:PAD_WIN yTop:y + 1];
self.popupHttpApplication = [[[[NSView popupButton:0] placeIn:self x:NSMaxX(l2.frame) + PAD_S yTop:y] sizeToRight:PAD_WIN]
action:@selector(changeHttpApplication:) target:controller];
// Notification type
y = YFromTop(self.popupHttpApplication) + PAD_M;
NSTextField *l3 = [[NSView label:NSLocalizedString(@"Notifications:", nil)] placeIn:self x:PAD_WIN yTop:y + 1];
self.popupNotificationType = [[[[NSView popupButton:0] placeIn:self x:NSMaxX(l3.frame) + PAD_S yTop:y] sizeToRight:PAD_WIN]
action:@selector(changeNotificationType:) target:controller];
return self;
}

View File

@@ -5,6 +5,7 @@
#import "UserPrefs.h"
#import "BarMenu.h"
#import "AppHook.h"
#import "NotifyEndpoint.h"
#import "NSView+Ext.h"
#import "NSColor+Ext.h"
@@ -71,12 +72,14 @@
- (void)setUnreadCountAbsolute:(NSUInteger)count {
_unreadCountTotal = (NSInteger)count;
[self updateBarIcon];
[NotifyEndpoint setGlobalCount:count];
}
/// Assign new value by adding @c count to total unread count (may be negative).
- (void)setUnreadCountRelative:(NSInteger)count {
_unreadCountTotal += count;
[self updateBarIcon];
[NotifyEndpoint setGlobalCount:(NSUInteger)_unreadCountTotal];
}
/// Fetch new total unread count from core data and assign it as new value (dispatch async on main thread).

View File

@@ -5,6 +5,7 @@
#import "FeedGroup+Ext.h"
#import "Constants.h"
#import "MapUnreadTotal.h"
#import "NotifyEndpoint.h"
typedef NS_ENUM(NSInteger, MenuItemTag) {
/// Used in @c allowDisplayOfHeaderItem: to identify and enable items
@@ -181,7 +182,8 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
}
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
NSArray<FeedArticle*> *list = [StoreCoordinator articlesAtPath:path isFeed:isFeedMenu sorted:openLinks unread:markRead inContext:moc limit:limit];
[StoreCoordinator updateArticles:list markRead:markRead andOpen:openLinks inContext:moc];
[NotifyEndpoint dismiss:
[StoreCoordinator updateArticles:list markRead:markRead andOpen:openLinks inContext:moc]];
}
@end