Fix adding feeds when offline or paused

This commit is contained in:
relikd
2019-08-14 16:47:34 +02:00
parent e6f4d05213
commit 9e7eda692b
14 changed files with 73 additions and 64 deletions

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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.

View File

@@ -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

View File

@@ -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).

View File

@@ -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];

View File

@@ -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;

View File

@@ -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

View File

@@ -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;

View File

@@ -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));
});
}

View File

@@ -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>

View File

@@ -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];
}];
}

View File

@@ -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];
}];

View File

@@ -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