feat: notifications
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 -
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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]; }
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
18
baRSS/Notifications/NotifyEndpoint.h
Normal file
18
baRSS/Notifications/NotifyEndpoint.h
Normal 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
|
||||
171
baRSS/Notifications/NotifyEndpoint.m
Normal file
171
baRSS/Notifications/NotifyEndpoint.m
Normal 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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user