8 Commits

Author SHA1 Message Date
relikd
5894b12c1d fix: user provided feed title for notifications (fixes #22) 2025-10-29 18:27:32 +01:00
relikd
0700eebb13 chore: update min OS in readme 2025-10-29 15:19:50 +01:00
relikd
4c4a133fe2 chore: bump version 2025-10-29 15:12:43 +01:00
relikd
ccca329630 feat: notification open options 2025-10-29 15:10:06 +01:00
relikd
831159904c chore: update changelog + bump version 2025-10-27 17:41:37 +01:00
relikd
cf3e9e4b4a feat: simplify options for show-only-unread 2025-10-27 17:36:07 +01:00
relikd
184e5c0882 fix: update menu with show-only-unread 2025-10-27 17:16:31 +01:00
relikd
575d1eaec8 fix: flipped "show only unread" (closes #21) 2025-10-27 16:21:31 +01:00
10 changed files with 60 additions and 23 deletions

View File

@@ -5,9 +5,24 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [1.4.1] 2025-10-27 ## [1.5.3] 2025-10-29
### Fixed
- *Notifications:* Use user-provided feed title instead of server provided title
## [1.5.2] 2025-10-29
### Added ### Added
- Notifications - *Notifications:* Reply with "Open in background", "Mark read & dismiss", or "Open but keep unread"
## [1.5.1] 2025-10-27
### Fixed
- *Status Bar Menu:* Simplified options for "Show only unread"
## [1.5.0] 2025-10-27
### Added
- *UI:* Notifications
## [1.4.1] 2025-07-29 ## [1.4.1] 2025-07-29
@@ -215,6 +230,9 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
Initial release Initial release
[1.5.3]: https://github.com/relikd/baRSS/compare/v1.5.2...v1.5.3
[1.5.2]: https://github.com/relikd/baRSS/compare/v1.5.1...v1.5.2
[1.5.1]: https://github.com/relikd/baRSS/compare/v1.5.0...v1.5.1
[1.5.0]: https://github.com/relikd/baRSS/compare/v1.4.1...v1.5.0 [1.5.0]: https://github.com/relikd/baRSS/compare/v1.4.1...v1.5.0
[1.4.1]: https://github.com/relikd/baRSS/compare/v1.4.0...v1.4.1 [1.4.1]: https://github.com/relikd/baRSS/compare/v1.4.0...v1.4.1
[1.4.0]: https://github.com/relikd/baRSS/compare/v1.3.2...v1.4.0 [1.4.0]: https://github.com/relikd/baRSS/compare/v1.3.2...v1.4.0

View File

@@ -1,4 +1,4 @@
[![macOS 10.13+](https://img.shields.io/badge/macOS-10.13+-888)](#download--install) [![macOS 10.14+](https://img.shields.io/badge/macOS-10.14+-888)](#download--install)
[![Current release](https://img.shields.io/github/release/relikd/baRSS)](https://github.com/relikd/baRSS/releases) [![Current release](https://img.shields.io/github/release/relikd/baRSS)](https://github.com/relikd/baRSS/releases)
[![All downloads](https://img.shields.io/github/downloads/relikd/baRSS/total)](https://github.com/relikd/baRSS/releases) [![All downloads](https://img.shields.io/github/downloads/relikd/baRSS/total)](https://github.com/relikd/baRSS/releases)
[![GitHub license](https://img.shields.io/github/license/relikd/baRSS)](LICENSE) [![GitHub license](https://img.shields.io/github/license/relikd/baRSS)](LICENSE)
@@ -35,7 +35,7 @@ Further, tuning the update frequently will decrease the traffic even more.
Download & Install Download & Install
------------------ ------------------
Requires macOS High Sierra (10.13) or higher. Requires macOS Mojave (10.14) or higher.
### Easy way ### Easy way
Go to [releases](https://github.com/relikd/baRSS/releases) and downloaded the latest version. Go to [releases](https://github.com/relikd/baRSS/releases) and downloaded the latest version.

View File

@@ -805,7 +805,7 @@
CLANG_WARN_UNREACHABLE_CODE = YES; CLANG_WARN_UNREACHABLE_CODE = YES;
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 16711; CURRENT_PROJECT_VERSION = 16785;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = dwarf; DEBUG_INFORMATION_FORMAT = dwarf;
DEVELOPMENT_TEAM = UY657LKNHJ; DEVELOPMENT_TEAM = UY657LKNHJ;
@@ -823,7 +823,7 @@
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 1.5.0; MARKETING_VERSION = 1.5.3;
MTL_ENABLE_DEBUG_INFO = YES; MTL_ENABLE_DEBUG_INFO = YES;
ONLY_ACTIVE_ARCH = YES; ONLY_ACTIVE_ARCH = YES;
SDKROOT = macosx; SDKROOT = macosx;
@@ -866,7 +866,7 @@
CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; CLANG_WARN__DUPLICATE_METHOD_MATCH = YES;
CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO; CODE_SIGN_INJECT_BASE_ENTITLEMENTS = NO;
COPY_PHASE_STRIP = NO; COPY_PHASE_STRIP = NO;
CURRENT_PROJECT_VERSION = 16711; CURRENT_PROJECT_VERSION = 16785;
DEAD_CODE_STRIPPING = YES; DEAD_CODE_STRIPPING = YES;
DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym";
DEVELOPMENT_TEAM = UY657LKNHJ; DEVELOPMENT_TEAM = UY657LKNHJ;
@@ -881,7 +881,7 @@
GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_FUNCTION = YES;
GCC_WARN_UNUSED_VARIABLE = YES; GCC_WARN_UNUSED_VARIABLE = YES;
MACOSX_DEPLOYMENT_TARGET = 10.14; MACOSX_DEPLOYMENT_TARGET = 10.14;
MARKETING_VERSION = 1.5.0; MARKETING_VERSION = 1.5.3;
MTL_ENABLE_DEBUG_INFO = NO; MTL_ENABLE_DEBUG_INFO = NO;
SDKROOT = macosx; SDKROOT = macosx;
}; };

View File

@@ -225,9 +225,11 @@
return nil; // if success == NO, do not modify unread state & exit return nil; // if success == NO, do not modify unread state & exit
} }
NSInteger countChange = 0;
for (FeedArticle *fa in list) { for (FeedArticle *fa in list) {
if (fa.unread == markRead) { // only if differs if (fa.unread == markRead) { // only if differs
fa.unread = !markRead; fa.unread = !markRead;
countChange += markRead ? -1 : +1;
} }
} }
[self saveContext:moc andParent:YES]; [self saveContext:moc andParent:YES];
@@ -242,8 +244,7 @@
} }
[moc reset]; [moc reset];
NSNumber *num = [NSNumber numberWithInteger: (markRead ? -1 : +1) * (NSInteger)list.count ]; PostNotification(kNotificationTotalUnreadCountChanged, @(countChange));
PostNotification(kNotificationTotalUnreadCountChanged, num);
return dbRefs; return dbRefs;
} }

View File

@@ -20,7 +20,6 @@
/** default: @c YES */ static NSString* const Pref_globalOpenUnread = @"globalOpenUnread"; /** default: @c YES */ static NSString* const Pref_globalOpenUnread = @"globalOpenUnread";
/** default: @c YES */ static NSString* const Pref_globalMarkRead = @"globalMarkRead"; /** default: @c YES */ static NSString* const Pref_globalMarkRead = @"globalMarkRead";
/** default: @c YES */ static NSString* const Pref_globalMarkUnread = @"globalMarkUnread"; /** default: @c YES */ static NSString* const Pref_globalMarkUnread = @"globalMarkUnread";
/** default: @c NO */ static NSString* const Pref_globalUnreadOnly = @"globalUnreadOnly";
/** default: @c YES */ static NSString* const Pref_globalUnreadCount = @"globalUnreadCount"; /** default: @c YES */ static NSString* const Pref_globalUnreadCount = @"globalUnreadCount";
/** default: @c YES */ static NSString* const Pref_groupOpenUnread = @"groupOpenUnread"; /** default: @c YES */ static NSString* const Pref_groupOpenUnread = @"groupOpenUnread";
/** default: @c YES */ static NSString* const Pref_groupMarkRead = @"groupMarkRead"; /** default: @c YES */ static NSString* const Pref_groupMarkRead = @"groupMarkRead";

View File

@@ -20,7 +20,7 @@ void UserPrefsInit(void) {
Pref_feedUnreadIndicator Pref_feedUnreadIndicator
]); ]);
defaultsAppend(defs, @NO, @[ defaultsAppend(defs, @NO, @[
Pref_globalUnreadOnly, Pref_groupUnreadOnly, Pref_feedUnreadOnly, Pref_groupUnreadOnly, Pref_feedUnreadOnly,
Pref_groupUnreadIndicator, Pref_groupUnreadIndicator,
Pref_feedTruncateTitle, Pref_feedTruncateTitle,
Pref_feedLimitArticles Pref_feedLimitArticles

View File

@@ -2,6 +2,7 @@
#import "UserPrefs.h" #import "UserPrefs.h"
#import "StoreCoordinator.h" #import "StoreCoordinator.h"
#import "Feed+Ext.h" #import "Feed+Ext.h"
#import "FeedGroup+Ext.h"
#import "FeedArticle+Ext.h" #import "FeedArticle+Ext.h"
/** /**
@@ -9,6 +10,11 @@
*/ */
static NSString* const kNotifyIdGlobal = @"global"; static NSString* const kNotifyIdGlobal = @"global";
static NSString* const kCategoryDismissable = @"DISMISSIBLE";
static NSString* const kActionOpenBackground = @"OPEN_IN_BACKGROUND";
static NSString* const kActionMarkRead = @"MARK_READ_DONT_OPEN";
static NSString* const kActionOpenOnly = @"OPEN_ONLY_DONT_MARK_READ";
@implementation NotifyEndpoint @implementation NotifyEndpoint
@@ -18,20 +24,27 @@ static NotificationType notifyType;
/// Ask user for permission to send notifications @b AND register delegate to respond to alert banner clicks. /// 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 /// @note Called every time user changes notification settings
+ (void)activate { + (void)activate {
UNUserNotificationCenter *center = UNUserNotificationCenter.currentNotificationCenter;
notifyType = UserPrefsNotificationType(); notifyType = UserPrefsNotificationType();
// even if disabled, register delegate. This allows to open previously sent notifications // even if disabled, register delegate. This allows to open previously sent notifications
static dispatch_once_t onceToken; static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{ dispatch_once(&onceToken, ^{
singleton = [NotifyEndpoint new]; singleton = [NotifyEndpoint new];
UNUserNotificationCenter.currentNotificationCenter.delegate = singleton; center.delegate = singleton;
}); });
if (notifyType == NotificationTypeDisabled) { if (notifyType == NotificationTypeDisabled) {
return; return;
} }
// register action types (allow mark read without opening notification)
UNNotificationAction *openBackgroundAction = [UNNotificationAction actionWithIdentifier:kActionOpenBackground title:NSLocalizedString(@"Open in background", nil) options:UNNotificationActionOptionNone];
UNNotificationAction *dontOpenAction = [UNNotificationAction actionWithIdentifier:kActionMarkRead title:NSLocalizedString(@"Mark read & dismiss", nil) options:UNNotificationActionOptionNone];
UNNotificationAction *dontReadAction = [UNNotificationAction actionWithIdentifier:kActionOpenOnly title:NSLocalizedString(@"Open but keep unread", nil) options:UNNotificationActionOptionNone];
UNNotificationCategory *category = [UNNotificationCategory categoryWithIdentifier:kCategoryDismissable actions:@[openBackgroundAction, dontOpenAction, dontReadAction] intentIdentifiers:@[] options:UNNotificationCategoryOptionNone];
[center setNotificationCategories:[NSSet setWithObject:category]];
[UNUserNotificationCenter.currentNotificationCenter requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionSound completionHandler:^(BOOL granted, NSError * _Nullable error) { [center requestAuthorizationWithOptions:UNAuthorizationOptionAlert | UNAuthorizationOptionSound completionHandler:^(BOOL granted, NSError * _Nullable error) {
if (error) { if (error) {
dispatch_async(dispatch_get_main_queue(), ^{ dispatch_async(dispatch_get_main_queue(), ^{
NSAlert *alert = [[NSAlert alloc] init]; NSAlert *alert = [[NSAlert alloc] init];
@@ -72,7 +85,7 @@ static NotificationType notifyType;
if (count > 0) { if (count > 0) {
[feed.managedObjectContext obtainPermanentIDsForObjects:@[feed] error:nil]; [feed.managedObjectContext obtainPermanentIDsForObjects:@[feed] error:nil];
[self send:feed.notificationID [self send:feed.notificationID
title:feed.title title:feed.group.anyName
body:[NSString stringWithFormat:NSLocalizedString(@"%ld unread articles", nil), count]]; body:[NSString stringWithFormat:NSLocalizedString(@"%ld unread articles", nil), count]];
} }
} }
@@ -84,7 +97,7 @@ static NotificationType notifyType;
} }
[article.managedObjectContext obtainPermanentIDsForObjects:@[article] error:nil]; [article.managedObjectContext obtainPermanentIDsForObjects:@[article] error:nil];
[self send:article.notificationID [self send:article.notificationID
title:article.feed.title title:article.feed.group.anyName
body:article.title]; body:article.title];
} }
@@ -105,6 +118,7 @@ static NotificationType notifyType;
if (title != nil) msg.title = title; if (title != nil) msg.title = title;
if (body != nil) msg.body = body; if (body != nil) msg.body = body;
// common settings: // common settings:
msg.categoryIdentifier = kCategoryDismissable;
// TODO: make sound configurable? // TODO: make sound configurable?
msg.sound = [UNNotificationSound defaultSound]; msg.sound = [UNNotificationSound defaultSound];
[self send:identifier content: msg]; [self send:identifier content: msg];
@@ -167,7 +181,12 @@ static NotificationType notifyType;
return; return;
} }
} }
[StoreCoordinator updateArticles:articles markRead:YES andOpen:YES inContext:moc];
// open-in-background performs the same operation as a normal click
// the "background" part is triggered by _NOT_ having the UNNotificationActionOptionForeground option
BOOL dontOpen = [response.actionIdentifier isEqualToString:kActionMarkRead];
BOOL dontMarkRead = [response.actionIdentifier isEqualToString:kActionOpenOnly];
[StoreCoordinator updateArticles:articles markRead:!dontMarkRead andOpen:!dontOpen inContext:moc];
} }
@end @end

View File

@@ -28,9 +28,9 @@
[self entry:NSLocalizedString(@"Open all unread", nil) c1:Pref_globalOpenUnread c2:Pref_groupOpenUnread c3:Pref_feedOpenUnread]; [self entry:NSLocalizedString(@"Open all unread", nil) c1:Pref_globalOpenUnread c2:Pref_groupOpenUnread c3:Pref_feedOpenUnread];
[self entry:NSLocalizedString(@"Mark all read", nil) c1:Pref_globalMarkRead c2:Pref_groupMarkRead c3:Pref_feedMarkRead]; [self entry:NSLocalizedString(@"Mark all read", nil) c1:Pref_globalMarkRead c2:Pref_groupMarkRead c3:Pref_feedMarkRead];
[self entry:NSLocalizedString(@"Mark all unread", nil) c1:Pref_globalMarkUnread c2:Pref_groupMarkUnread c3:Pref_feedMarkUnread]; [self entry:NSLocalizedString(@"Mark all unread", nil) c1:Pref_globalMarkUnread c2:Pref_groupMarkUnread c3:Pref_feedMarkUnread];
[self entry:NSLocalizedString(@"Show only unread / hide read", nil) c1:Pref_globalUnreadOnly c2:Pref_groupUnreadOnly c3:Pref_feedUnreadOnly];
[self entry:NSLocalizedString(@"Number of unread articles", nil) c1:Pref_globalUnreadCount c2:Pref_groupUnreadCount c3:Pref_feedUnreadCount]; [self entry:NSLocalizedString(@"Number of unread articles", nil) c1:Pref_globalUnreadCount c2:Pref_groupUnreadCount c3:Pref_feedUnreadCount];
[self entry:NSLocalizedString(@"Indicator for unread articles", nil) c1:nil c2:Pref_groupUnreadIndicator c3:Pref_feedUnreadIndicator]; [self entry:NSLocalizedString(@"Indicator for unread articles", nil) c1:nil c2:Pref_groupUnreadIndicator c3:Pref_feedUnreadIndicator];
[self entry:NSLocalizedString(@"Show only unread / hide read", nil) c1:nil c2:Pref_groupUnreadOnly c3:Pref_feedUnreadOnly];
[[self entry:NSLocalizedString(@"Truncate article title", nil) c1:nil c2:nil c3:Pref_feedTruncateTitle] [[self entry:NSLocalizedString(@"Truncate article title", nil) c1:nil c2:nil c3:Pref_feedTruncateTitle]
tooltip:NSLocalizedString(@"Truncate article title after 60 characters", nil)]; tooltip:NSLocalizedString(@"Truncate article title after 60 characters", nil)];
[[self entry:NSLocalizedString(@"Limit number of articles", nil) c1:nil c2:nil c3:Pref_feedLimitArticles] [[self entry:NSLocalizedString(@"Limit number of articles", nil) c1:nil c2:nil c3:Pref_feedLimitArticles]

View File

@@ -132,12 +132,12 @@
if (item) { // nil on last loop (aka main menu, see below) if (item) { // nil on last loop (aka main menu, see below)
[item.submenu setHeaderHasUnread:uct]; [item.submenu setHeaderHasUnread:uct];
[item setTitleCount:uct.unread]; [item setTitleCount:uct.unread];
item.hidden = NO;
item = item.parentItem; item = item.parentItem;
} }
} }
// call on main menu // call on main menu
[self.statusItem.mainMenu setHeaderHasUnread:itms.firstObject]; [self.statusItem.mainMenu setHeaderHasUnread:itms.firstObject];
// TODO: need to re-create groups if user chose to hide already read articles
} }
} }

View File

@@ -57,10 +57,10 @@ typedef NS_ENUM(NSInteger, MenuItemTag) {
NSUInteger unread = unreadMap[[t substringFromIndex:2]].unread; NSUInteger unread = unreadMap[[t substringFromIndex:2]].unread;
// Check user preferences to show only unread entries // Check user preferences to show only unread entries
if (unread == 0 && if (unread == 0
((fg.type == FEED && UserPrefsBool(Pref_groupUnreadOnly)) || && (fg.type == FEED || fg.type == GROUP)
(fg.type == GROUP && UserPrefsBool(Pref_globalUnreadOnly)))) { && UserPrefsBool(Pref_groupUnreadOnly)) {
return nil; item.hidden = YES;
} }
item.submenu = [[NSMenu alloc] initWithTitle:t]; item.submenu = [[NSMenu alloc] initWithTitle:t];