Fix adding feeds when offline or paused
This commit is contained in:
13
CHANGELOG.md
13
CHANGELOG.md
@@ -25,20 +25,25 @@ and this project does adhere to [Semantic Versioning](https://semver.org/spec/v2
|
||||
- *Adding feed:* Show users any 5xx server error response and extracted failure reason
|
||||
- *Adding feed:* If URLs can't be resolved in the first run (5xx error), try a second time. E.g., `Done` click (issue: #5)
|
||||
- *Adding feed:* Prefer favicons with size `32x32`
|
||||
- *Adding feed:* Inserting feeds when offline will postpone download until network is reachable again
|
||||
- *Adding feed:* Inserting feeds when paused will postpone download until unpaused
|
||||
- *Settings, Feeds:* Actions `delete` and `edit` use clicked items instead of selected items
|
||||
- *Settings, Feeds:* Accurate download count instead of `Updating feeds …`
|
||||
- *Status Bar Menu*: Feed title is updated properly
|
||||
- *UI:* If an error occurs, show document URL (path to file or web url)
|
||||
- Comparison of existing articles with nonexistent guid and link
|
||||
- Don't mark articles read if opening URLs failed
|
||||
- HTML tag removal keeps structure intact
|
||||
|
||||
### Changed
|
||||
- *UI:* Interface builder files replaced with code equivalent
|
||||
- *UI:* Mark unread articles with blue dot, instead of tick mark
|
||||
- *Settings, Feeds:* Single add button for feeds, groups, and separators
|
||||
- *Settings, Feeds:* Always append new items at the end
|
||||
- *Adding feed:* Display error reason if user cancels the creation of a new feed item
|
||||
- *Adding feed:* Refresh interval hotkeys set to: `⌘1` … `⌘6`
|
||||
- *Settings, Feeds:* Single add button for feeds, groups, and separators
|
||||
- *Settings, Feeds:* Always append new items at the end
|
||||
- *Status Bar Menu*: Show `(no title)` instead of `(error)`
|
||||
- *Status Bar Menu*: `Update all feeds` will show error alerts for broken URLs
|
||||
- *UI:* Interface builder files replaced with code equivalent
|
||||
- *UI:* Mark unread articles with blue dot, instead of tick mark
|
||||
- *DB*: New table for options. E.g., what app version modified the database
|
||||
|
||||
|
||||
|
||||
@@ -34,7 +34,7 @@
|
||||
/// Instantiates new @c Feed and @c FeedMeta entities in context.
|
||||
+ (instancetype)newFeedAndMetaInContext:(NSManagedObjectContext*)moc {
|
||||
Feed *feed = [[Feed alloc] initWithEntity:Feed.entity insertIntoManagedObjectContext:moc];
|
||||
feed.meta = [[FeedMeta alloc] initWithEntity:FeedMeta.entity insertIntoManagedObjectContext:moc];
|
||||
feed.meta = [FeedMeta newMetaInContext:moc];
|
||||
return feed;
|
||||
}
|
||||
|
||||
@@ -43,7 +43,6 @@
|
||||
NSUInteger lastIndex = [StoreCoordinator countRootItemsInContext:moc];
|
||||
FeedGroup *fg = [FeedGroup newGroup:FEED inContext:moc];
|
||||
[fg setParent:nil andSortIndex:(int32_t)lastIndex];
|
||||
[fg.feed.meta setRefreshAndSchedule:kDefaultFeedRefreshInterval];
|
||||
return fg.feed;
|
||||
}
|
||||
|
||||
|
||||
@@ -29,9 +29,9 @@
|
||||
|
||||
#pragma mark - Properties
|
||||
|
||||
/// @return Returns "(error)" if @c self.name is @c nil.
|
||||
/// @return Returns "(no title)" if @c self.name is @c nil.
|
||||
- (nonnull NSString*)nameOrError {
|
||||
return (self.name ? self.name : NSLocalizedString(@"(error)", nil));
|
||||
return (self.name ? self.name : NSLocalizedString(@"(no title)", nil));
|
||||
}
|
||||
|
||||
/// @return Return @c 16x16px NSImageNameFolder image.
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
static int32_t const kDefaultFeedRefreshInterval = 30 * 60;
|
||||
|
||||
@interface FeedMeta (Ext)
|
||||
+ (instancetype)newMetaInContext:(NSManagedObjectContext*)moc;
|
||||
// HTTP response
|
||||
- (void)setErrorAndPostponeSchedule;
|
||||
- (void)setSucessfulWithResponse:(NSHTTPURLResponse*)response;
|
||||
@@ -33,4 +34,5 @@ static int32_t const kDefaultFeedRefreshInterval = 30 * 60;
|
||||
- (void)setUrlIfChanged:(NSString*)url;
|
||||
- (void)setEtag:(NSString*)etag modified:(NSString*)modified;
|
||||
- (BOOL)setRefreshAndSchedule:(int32_t)refresh;
|
||||
- (void)scheduleNow:(NSTimeInterval)future;
|
||||
@end
|
||||
|
||||
@@ -26,6 +26,14 @@
|
||||
|
||||
@implementation FeedMeta (Ext)
|
||||
|
||||
/// Create new instance with default @c refresh interval and set @c scheduled to distant past.
|
||||
+ (instancetype)newMetaInContext:(NSManagedObjectContext*)moc {
|
||||
FeedMeta *meta = [[FeedMeta alloc] initWithEntity:FeedMeta.entity insertIntoManagedObjectContext:moc];
|
||||
meta.refresh = kDefaultFeedRefreshInterval;
|
||||
meta.scheduled = [NSDate distantPast]; // will cause update to refresh as soon as possible
|
||||
return meta;
|
||||
}
|
||||
|
||||
#pragma mark - HTTP response
|
||||
|
||||
/// Increment @c errorCount and set new @c scheduled date (2^N minutes, max. 5.7 days).
|
||||
|
||||
@@ -143,7 +143,7 @@ NS_INLINE NSInteger RadioGroupSelection(NSView *view) {
|
||||
interval = (int32_t)[refresh integerValue];
|
||||
|
||||
newFeed.feed.meta.url = [item attributeForKey:OPMLXMLURLKey];
|
||||
[newFeed.feed.meta setRefreshAndSchedule:interval];
|
||||
newFeed.feed.meta.refresh = interval;
|
||||
} else { // GROUP
|
||||
for (NSUInteger i = 0; i < item.children.count; i++) {
|
||||
[self importFeed:item.children[i] parent:newFeed index:(int32_t)i inContext:moc];
|
||||
|
||||
@@ -22,6 +22,8 @@
|
||||
|
||||
@import Cocoa;
|
||||
|
||||
@class Feed;
|
||||
|
||||
@interface UpdateScheduler : NSObject
|
||||
@property (class, readonly) NSUInteger feedsInQueue;
|
||||
@property (class, readonly) NSDate *dateScheduled;
|
||||
@@ -29,10 +31,9 @@
|
||||
@property (class, readonly) BOOL isUpdating;
|
||||
@property (class, setter=setPaused:) BOOL isPaused;
|
||||
|
||||
+ (void)beginUpdate;
|
||||
+ (void)endUpdate;
|
||||
+ (void)scheduleNextFeed;
|
||||
+ (void)forceUpdateAllFeeds;
|
||||
+ (void)downloadList:(NSArray<Feed*>*)list background:(BOOL)flag finally:(nullable os_block_t)block;
|
||||
// Register for network change notifications
|
||||
+ (void)registerNetworkChangeNotification;
|
||||
+ (void)unregisterNetworkChangeNotification;
|
||||
|
||||
@@ -28,8 +28,7 @@
|
||||
|
||||
static NSTimer *_timer;
|
||||
static SCNetworkReachabilityRef _reachability = NULL;
|
||||
static BOOL _isReachable = NO;
|
||||
static BOOL _isUpdating = NO;
|
||||
static BOOL _isReachable = YES;
|
||||
static BOOL _updatePaused = NO;
|
||||
static BOOL _nextUpdateIsForced = NO;
|
||||
|
||||
@@ -48,13 +47,14 @@ static BOOL _nextUpdateIsForced = NO;
|
||||
+ (BOOL)allowNetworkConnection { return (_isReachable && !_updatePaused); }
|
||||
|
||||
/// @return @c YES if batch update is running
|
||||
+ (BOOL)isUpdating { return _isUpdating; }
|
||||
+ (BOOL)isUpdating { return [WebFeed feedsInQueue] > 0; }
|
||||
|
||||
/// @return @c YES if update is paused by user.
|
||||
+ (BOOL)isPaused { return _updatePaused; }
|
||||
|
||||
/// Set paused flag and cancel timer regardless of network connectivity.
|
||||
+ (void)setPaused:(BOOL)flag {
|
||||
// TODO: should pause persist between app launches?
|
||||
_updatePaused = flag;
|
||||
if (_updatePaused)
|
||||
[self pauseUpdates];
|
||||
@@ -73,12 +73,6 @@ static BOOL _nextUpdateIsForced = NO;
|
||||
[self scheduleNextFeed];
|
||||
}
|
||||
|
||||
/// Set @c isUpdating @c = @c YES
|
||||
+ (void)beginUpdate { _isUpdating = YES; }
|
||||
|
||||
/// Set @c isUpdating @c = @c NO
|
||||
+ (void)endUpdate { _isUpdating = NO; }
|
||||
|
||||
|
||||
#pragma mark - Update Feed Timer
|
||||
|
||||
@@ -133,25 +127,34 @@ static BOOL _nextUpdateIsForced = NO;
|
||||
#endif
|
||||
BOOL updateAll = _nextUpdateIsForced;
|
||||
_nextUpdateIsForced = NO;
|
||||
if (updateAll)
|
||||
[WebFeed setRequestsAreUrgent:YES];
|
||||
|
||||
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
|
||||
NSArray<Feed*> *list = [StoreCoordinator getListOfFeedsThatNeedUpdate:updateAll inContext:moc];
|
||||
//NSAssert(list.count > 0, @"ERROR: Something went wrong, timer fired too early.");
|
||||
if (![self allowNetworkConnection]) {
|
||||
[WebFeed setRequestsAreUrgent:NO];
|
||||
[moc reset];
|
||||
return;
|
||||
}
|
||||
[WebFeed batchDownloadFeeds:list favicons:updateAll showErrorAlert:NO finally:^{
|
||||
[WebFeed setRequestsAreUrgent:NO];
|
||||
|
||||
[self downloadList:list background:!updateAll finally:^{
|
||||
[StoreCoordinator saveContext:moc andParent:YES]; // save parents too ...
|
||||
[moc reset];
|
||||
[self resumeUpdates]; // always reset the timer
|
||||
}];
|
||||
}
|
||||
|
||||
/// Download list of feeds. Either silently in background or in foreground with alerts.
|
||||
+ (void)downloadList:(NSArray<Feed*>*)list background:(BOOL)flag finally:(nullable os_block_t)block {
|
||||
if (![self allowNetworkConnection]) {
|
||||
if (block) block();
|
||||
} else if (flag) {
|
||||
[WebFeed batchDownloadFeeds:list showErrorAlert:NO finally:block];
|
||||
} else {
|
||||
// TODO: add undo grouping?
|
||||
[WebFeed setRequestsAreUrgent:YES];
|
||||
[WebFeed batchDownloadFeeds:list showErrorAlert:YES finally:^{
|
||||
[WebFeed setRequestsAreUrgent:NO];
|
||||
if (block) block();
|
||||
}];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#pragma mark - Network Connection & Reachability
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
+ (void)newFeed:(NSString *)urlStr askUser:(nonnull NSString*(^)(RSHTMLMetadata *meta))askUser block:(nonnull void(^)(RSParsedFeed *parsed, NSError *error, NSHTTPURLResponse *response))block;
|
||||
+ (void)autoDownloadAndParseURL:(NSString*)urlStr addAnyway:(BOOL)flag modify:(nullable void(^)(Feed *feed))block;
|
||||
+ (void)autoDownloadAndParseUpdateURL;
|
||||
+ (void)batchDownloadFeeds:(NSArray<Feed*> *)list favicons:(BOOL)fav showErrorAlert:(BOOL)alert finally:(nullable os_block_t)block;
|
||||
+ (void)batchDownloadFeeds:(NSArray<Feed*>*)list showErrorAlert:(BOOL)alert finally:(nullable os_block_t)block;
|
||||
// Favicon image download
|
||||
+ (void)downloadFavicon:(NSString*)urlStr finished:(void(^)(NSImage * _Nullable img))block;
|
||||
+ (void)downloadImage:(NSString*)url finished:(void(^)(NSImage * _Nullable img))block;
|
||||
|
||||
@@ -233,18 +233,12 @@ static _Atomic(NSUInteger) _queueSize = 0;
|
||||
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
|
||||
Feed *f = [Feed appendToRootWithDefaultIntervalInContext:moc];
|
||||
f.meta.url = url;
|
||||
[self backgroundUpdateBoth:f favicon:YES alert:!flag finally:^(BOOL successful){
|
||||
if (!flag && !successful) {
|
||||
[moc deleteObject:f.group];
|
||||
} else if (block) {
|
||||
block(f); // only on success
|
||||
}
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
if (block) block(f);
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
[UpdateScheduler downloadList:@[f] background:flag finally:^{
|
||||
PostNotification(kNotificationGroupInserted, f.group.objectID);
|
||||
[moc reset];
|
||||
if (successful) {
|
||||
PostNotification(kNotificationGroupInserted, f.group.objectID);
|
||||
[UpdateScheduler scheduleNextFeed];
|
||||
}
|
||||
[UpdateScheduler scheduleNextFeed];
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -252,20 +246,20 @@ static _Atomic(NSUInteger) _queueSize = 0;
|
||||
+ (void)autoDownloadAndParseUpdateURL {
|
||||
[self autoDownloadAndParseURL:versionUpdateURL addAnyway:YES modify:^(Feed *feed) {
|
||||
feed.group.name = NSLocalizedString(@"baRSS releases", nil);
|
||||
[feed.meta setRefreshAndSchedule:2 * TimeUnitDays];
|
||||
feed.meta.refresh = 2 * TimeUnitDays;
|
||||
}];
|
||||
}
|
||||
|
||||
/**
|
||||
Start download of feed xml, then continue with favicon download (optional).
|
||||
Start download of feed xml, then continue with favicon (if newly added or 'Update all').
|
||||
|
||||
@param fav If @c YES continue with favicon download after xml download finished.
|
||||
@param alert If @c YES display Error Popup to user.
|
||||
@param block Parameter @c success is @c YES if xml download succeeded (regardless of favicon result).
|
||||
*/
|
||||
+ (void)backgroundUpdateBoth:(Feed*)feed favicon:(BOOL)fav alert:(BOOL)alert finally:(nullable void(^)(BOOL success))block {
|
||||
+ (void)backgroundUpdateBoth:(Feed*)feed alert:(BOOL)alert finally:(nullable void(^)(BOOL success))block {
|
||||
BOOL recentlyAdded = (feed.articles.count == 0);
|
||||
[self backgroundUpdateFeed:feed showErrorAlert:alert finally:^(BOOL success) {
|
||||
if (fav && success) {
|
||||
if (success && (recentlyAdded || _requestsAreUrgent)) {
|
||||
[self backgroundUpdateFavicon:feed replaceExisting:NO finally:^{
|
||||
if (block) block(YES);
|
||||
}];
|
||||
@@ -276,21 +270,19 @@ static _Atomic(NSUInteger) _queueSize = 0;
|
||||
}
|
||||
|
||||
/**
|
||||
Start download of all feeds in list. Either with or without favicons.
|
||||
Start download of all feeds in list. Favicons will be loaded for new feeds and for 'Update all'.
|
||||
|
||||
@param list Download list using @c feed.meta.url as download url. (while reusing etag and modified headers)
|
||||
@param fav If @c YES continue with favicon download after xml download finished.
|
||||
@param alert If @c YES display Error Popup to user.
|
||||
@param block Called after all downloads finished.
|
||||
*/
|
||||
+ (void)batchDownloadFeeds:(NSArray<Feed*> *)list favicons:(BOOL)fav showErrorAlert:(BOOL)alert finally:(nullable os_block_t)block {
|
||||
[UpdateScheduler beginUpdate];
|
||||
+ (void)batchDownloadFeeds:(NSArray<Feed*>*)list showErrorAlert:(BOOL)alert finally:(nullable os_block_t)block {
|
||||
atomic_fetch_add_explicit(&_queueSize, list.count, memory_order_relaxed);
|
||||
PostNotification(kNotificationBackgroundUpdateInProgress, @(_queueSize));
|
||||
dispatch_group_t group = dispatch_group_create();
|
||||
for (Feed *f in list) {
|
||||
dispatch_group_enter(group);
|
||||
[self backgroundUpdateBoth:f favicon:fav alert:alert finally:^(BOOL success){
|
||||
[self backgroundUpdateBoth:f alert:alert finally:^(BOOL success){
|
||||
atomic_fetch_sub_explicit(&_queueSize, 1, memory_order_relaxed);
|
||||
PostNotification(kNotificationBackgroundUpdateInProgress, @(_queueSize));
|
||||
dispatch_group_leave(group);
|
||||
@@ -298,8 +290,7 @@ static _Atomic(NSUInteger) _queueSize = 0;
|
||||
}
|
||||
dispatch_group_notify(group, dispatch_get_main_queue(), ^{
|
||||
if (block) block();
|
||||
[UpdateScheduler endUpdate];
|
||||
PostNotification(kNotificationBackgroundUpdateInProgress, @(0));
|
||||
PostNotification(kNotificationBackgroundUpdateInProgress, @(_queueSize));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -60,7 +60,7 @@
|
||||
</dict>
|
||||
</array>
|
||||
<key>CFBundleVersion</key>
|
||||
<string>10678</string>
|
||||
<string>10782</string>
|
||||
<key>LSApplicationCategoryType</key>
|
||||
<string>public.app-category.news</string>
|
||||
<key>LSMinimumSystemVersion</key>
|
||||
|
||||
@@ -72,11 +72,10 @@
|
||||
@property (strong) RefreshStatisticsView *statisticsView;
|
||||
|
||||
@property (copy) NSString *previousURL; // check if changed and avoid multiple download
|
||||
@property (copy) NSString *httpDate;
|
||||
@property (copy) NSString *httpEtag;
|
||||
@property (copy) NSString *faviconURL;
|
||||
@property (strong) NSError *feedError; // download error or xml parser error
|
||||
@property (strong) RSParsedFeed *feedResult; // parsed result
|
||||
@property (strong) NSHTTPURLResponse *httpResponse;
|
||||
@property (assign) BOOL didDownloadFeed; // check if feed articles need update
|
||||
@end
|
||||
|
||||
@@ -119,7 +118,7 @@
|
||||
[meta setRefreshAndSchedule:[NSDate intervalForPopup:self.view.refreshUnit andField:self.view.refreshNum]];
|
||||
// updateTimer will be scheduled once preferences is closed
|
||||
if (self.didDownloadFeed) {
|
||||
[meta setEtag:self.httpEtag modified:self.httpDate];
|
||||
[meta setSucessfulWithResponse:self.httpResponse];
|
||||
[feed updateWithRSS:self.feedResult postUnreadCountChange:YES];
|
||||
[feed setIconImage:self.view.favicon.image];
|
||||
}
|
||||
@@ -141,10 +140,9 @@
|
||||
if ([self.view.name.stringValue isEqualToString:self.feedResult.title]) {
|
||||
self.view.name.stringValue = @"";
|
||||
}
|
||||
self.feedResult = nil;
|
||||
self.feedError = nil;
|
||||
self.httpEtag = nil;
|
||||
self.httpDate = nil;
|
||||
self.feedResult = nil;
|
||||
self.httpResponse = nil;
|
||||
self.faviconURL = nil;
|
||||
self.previousURL = self.view.url.stringValue;
|
||||
}
|
||||
@@ -167,8 +165,7 @@
|
||||
self.didDownloadFeed = YES;
|
||||
self.feedResult = result;
|
||||
self.feedError = error;
|
||||
self.httpEtag = [response allHeaderFields][@"Etag"];
|
||||
self.httpDate = [response allHeaderFields][@"Date"]; // @"Expires", @"Last-Modified"
|
||||
self.httpResponse = response;
|
||||
[self postDownload:response.URL.absoluteString];
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
#import "SettingsFeeds+DragDrop.h"
|
||||
#import "StoreCoordinator.h"
|
||||
#import "Constants.h"
|
||||
#import "WebFeed.h"
|
||||
#import "UpdateScheduler.h"
|
||||
#import "FeedGroup+Ext.h"
|
||||
|
||||
// Pasteboard type used during internal row reordering
|
||||
@@ -160,7 +160,8 @@ const NSPasteboardType dragReorder = @"de.relikd.baRSS.drag-reorder";
|
||||
[StoreCoordinator saveContext:moc andParent:YES];
|
||||
if (selection.count > 0)
|
||||
[self.dataStore setSelectionIndexPaths:[selection sortedArrayUsingSelector:@selector(compare:)]];
|
||||
[WebFeed batchDownloadFeeds:feedsList favicons:YES showErrorAlert:YES finally:^{
|
||||
|
||||
[UpdateScheduler downloadList:feedsList background:NO finally:^{
|
||||
[self endCoreDataChangeUndoEmpty:NO forceUndo:NO];
|
||||
[self someDatesChangedScheduleUpdateTimer];
|
||||
}];
|
||||
|
||||
@@ -154,6 +154,8 @@
|
||||
[self.unreadMap updateAllCounts:updated forPath:feed.indexPath];
|
||||
// 2. rebuild articles menu if it is open
|
||||
if (item.submenu.isFeedMenu) { // menu item is visible
|
||||
if (feed.group.name)
|
||||
item.title = feed.group.name; // will replace (no title)
|
||||
item.image = [feed iconImage16];
|
||||
item.enabled = (feed.articles.count > 0);
|
||||
if (item.submenu.numberOfItems > 0) { // replace articles menu
|
||||
|
||||
Reference in New Issue
Block a user