Make 'feed://' URLs clickable. Append feeds automatically to root.

This commit is contained in:
relikd
2018-12-11 14:57:40 +01:00
parent 821e40a68b
commit 59d0ec7cca
10 changed files with 159 additions and 60 deletions

View File

@@ -1 +1 @@
github "relikd/RSXML" "6bf8f713596c1d3e253780cf7f6bd62843dc12a7" github "relikd/RSXML" "f012a6fa3cb8882a17762d92f3c41e49abfd3985"

View File

@@ -58,7 +58,7 @@ ToDo
- [ ] Automatically choose best interval? - [ ] Automatically choose best interval?
- [ ] Show time of next update - [ ] Show time of next update
- [x] Auto fix 301 Redirect or ask user - [x] Auto fix 301 Redirect or ask user
- [ ] Make `feed://` URLs clickable - [x] Make `feed://` URLs clickable
- [ ] Feeds with authentication - [ ] Feeds with authentication
- [ ] Show proper feed icon - [ ] Show proper feed icon
- [ ] Download and store icon file - [ ] Download and store icon file
@@ -86,7 +86,7 @@ ToDo
- [ ] Notification Center - [ ] Notification Center
- [ ] Sleep timer. (e.g., disable updates during working hours) - [ ] Sleep timer. (e.g., disable updates during working hours)
- [ ] Pure image feed? (show images directly in menu) - [ ] Pure image feed? (show images directly in menu)
- ~~[ ] Infinite storage. (load more button)~~ - [ ] ~~Infinite storage. (load more button)~~
- [ ] Automatically open feed items? - [ ] Automatically open feed items?
- [ ] Per feed launch application (e.g., for podcasts) - [ ] Per feed launch application (e.g., for podcasts)
- [ ] Per group setting to exclude unread count from menu bar - [ ] Per group setting to exclude unread count from menu bar

View File

@@ -22,6 +22,7 @@
#import "AppHook.h" #import "AppHook.h"
#import "BarMenu.h" #import "BarMenu.h"
#import "FeedDownload.h"
@implementation AppHook @implementation AppHook
@@ -40,17 +41,23 @@
- (void)applicationDidFinishLaunching:(NSNotification *)aNotification { - (void)applicationDidFinishLaunching:(NSNotification *)aNotification {
printf("up and running\n"); printf("up and running\n");
// https://feeds.feedburner.com/simpledesktops // feed://https://feeds.feedburner.com/simpledesktops
[FeedDownload registerNetworkChangeNotification]; // will call update scheduler
} }
- (void)applicationWillTerminate:(NSNotification *)aNotification { - (void)applicationWillTerminate:(NSNotification *)aNotification {
[FeedDownload unregisterNetworkChangeNotification];
} }
- (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent { - (void)handleGetURLEvent:(NSAppleEventDescriptor *)event withReplyEvent:(NSAppleEventDescriptor *)replyEvent {
NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue]; NSString *url = [[event paramDescriptorForKeyword:keyDirectObject] stringValue];
// TODO: Open feed edit sheet in preferences if ([url hasPrefix:@"feed:"]) {
NSLog(@"%@", url); // TODO: handle other app schemes like configuration export / import
url = [url substringFromIndex:5];
if ([url hasPrefix:@"//"])
url = [url substringFromIndex:2];
[FeedDownload autoDownloadAndParseURL:url];
}
} }

View File

@@ -44,8 +44,10 @@
self.indexPath = pthStr; self.indexPath = pthStr;
} }
#pragma mark - Update Feed Items - #pragma mark - Update Feed Items -
/** /**
Replace feed title, subtitle and link (if changed). Also adds new articles and removes old ones. Replace feed title, subtitle and link (if changed). Also adds new articles and removes old ones.
*/ */
@@ -55,10 +57,15 @@
if (![self.link isEqualToString:obj.link]) self.link = obj.link; if (![self.link isEqualToString:obj.link]) self.link = obj.link;
int32_t unreadBefore = self.unreadCount; int32_t unreadBefore = self.unreadCount;
// Add and remove articles
NSMutableSet<NSString*> *urls = [[self.articles valueForKeyPath:@"link"] mutableCopy]; NSMutableSet<NSString*> *urls = [[self.articles valueForKeyPath:@"link"] mutableCopy];
[self addMissingArticles:obj updateLinks:urls]; // will remove links in 'urls' that should be kept [self addMissingArticles:obj updateLinks:urls]; // will remove links in 'urls' that should be kept
if (urls.count > 0) if (urls.count > 0)
[self deleteArticlesWithLink:urls]; // remove old, outdated articles [self deleteArticlesWithLink:urls]; // remove old, outdated articles
// Get new total article count and post unread-count-change notification
int32_t totalCount = (int32_t)self.articles.count;
if (self.articleCount != totalCount)
self.articleCount = totalCount;
if (flag) { if (flag) {
NSNumber *cDiff = [NSNumber numberWithInteger:self.unreadCount - unreadBefore]; NSNumber *cDiff = [NSNumber numberWithInteger:self.unreadCount - unreadBefore];
[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged object:cDiff]; [[NSNotificationCenter defaultCenter] postNotificationName:kNotificationTotalUnreadCountChanged object:cDiff];
@@ -66,35 +73,56 @@
} }
/** /**
Append new articles and increment their sortIndex. Update article counter and unread counter on the way. Append new articles and increment their sortIndex. Update unread counter on the way.
@note
New articles should be in ascending order without any gaps in between.
If new article is disjunct from the article before, assume a deleted article re-appeared and mark it as read.
@param urls Input will be used to identify new articles. Output will contain URLs that aren't present in the feed anymore. @param urls Input will be used to identify new articles. Output will contain URLs that aren't present in the feed anymore.
@return @c YES if new items were added, @c NO otherwise.
*/ */
- (BOOL)addMissingArticles:(RSParsedFeed*)obj updateLinks:(NSMutableSet<NSString*>*)urls { - (void)addMissingArticles:(RSParsedFeed*)obj updateLinks:(NSMutableSet<NSString*>*)urls {
int latestID = [[self.articles valueForKeyPath:@"@max.sortIndex"] intValue]; int32_t newOnes = 0;
__block int newOnes = 0; int32_t currentIndex = [[self.articles valueForKeyPath:@"@min.sortIndex"] intValue];
[obj.articles enumerateObjectsWithOptions:NSEnumerationReverse usingBlock:^(RSParsedArticle * _Nonnull article, BOOL * _Nonnull stop) { FeedArticle *lastInserted = nil;
BOOL hasGapBetweenNewArticles = NO;
for (RSParsedArticle *article in [obj.articles reverseObjectEnumerator]) {
// reverse enumeration ensures correct article order // reverse enumeration ensures correct article order
if ([urls containsObject:article.link]) { if ([urls containsObject:article.link]) {
[urls removeObject:article.link]; [urls removeObject:article.link];
FeedArticle *storedArticle = [self findArticleWithLink:article.link]; // TODO: use two synced arrays?
if (storedArticle && storedArticle.sortIndex != currentIndex) {
storedArticle.sortIndex = currentIndex;
}
hasGapBetweenNewArticles = YES;
} else { } else {
newOnes += 1; newOnes += 1;
[self insertArticle:article atIndex:latestID + newOnes]; if (hasGapBetweenNewArticles && lastInserted) { // gap with at least one article inbetween
lastInserted.unread = NO;
NSLog(@"Ghost item: %@", lastInserted.title);
newOnes -= 1;
}
hasGapBetweenNewArticles = NO;
lastInserted = [self insertArticle:article atIndex:currentIndex];
} }
}]; currentIndex += 1;
if (newOnes == 0) return NO; }
self.articleCount += newOnes; if (hasGapBetweenNewArticles && lastInserted) {
self.unreadCount += newOnes; // new articles are by definition unread lastInserted.unread = NO;
return YES; NSLog(@"Ghost item: %@", lastInserted.title);
newOnes -= 1;
}
if (newOnes > 0)
self.unreadCount += newOnes; // new articles are by definition unread
} }
/** /**
Create article based on input and insert into core data storage. Create article based on input and insert into core data storage.
*/ */
- (void)insertArticle:(RSParsedArticle*)entry atIndex:(int)idx { - (FeedArticle*)insertArticle:(RSParsedArticle*)entry atIndex:(int32_t)idx {
FeedArticle *fa = [[FeedArticle alloc] initWithEntity:FeedArticle.entity insertIntoManagedObjectContext:self.managedObjectContext]; FeedArticle *fa = [[FeedArticle alloc] initWithEntity:FeedArticle.entity insertIntoManagedObjectContext:self.managedObjectContext];
fa.sortIndex = (int32_t)idx; fa.sortIndex = idx;
fa.unread = YES; fa.unread = YES;
fa.guid = entry.guid; fa.guid = entry.guid;
fa.title = entry.title; fa.title = entry.title;
@@ -104,6 +132,7 @@
fa.link = entry.link; fa.link = entry.link;
fa.published = entry.datePublished; fa.published = entry.datePublished;
[self addArticlesObject:fa]; [self addArticlesObject:fa];
return fa;
} }
/** /**
@@ -126,8 +155,10 @@
} }
} }
#pragma mark - Article Properties - #pragma mark - Article Properties -
/** /**
@return Articles sorted by attribute @c sortIndex with descending order (newest items first). @return Articles sorted by attribute @c sortIndex with descending order (newest items first).
*/ */
@@ -137,6 +168,17 @@
return [self.articles sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:NO]]]; return [self.articles sortedArrayUsingDescriptors:@[[NSSortDescriptor sortDescriptorWithKey:@"sortIndex" ascending:NO]]];
} }
/**
Iterate over all Articles and return the one where @c .link matches. Or @c nil if no matching article found.
*/
- (FeedArticle*)findArticleWithLink:(NSString*)url {
for (FeedArticle *a in self.articles) {
if ([a.link isEqualToString:url])
return a;
}
return nil;
}
/** /**
For all articles set @c unread @c = @c NO For all articles set @c unread @c = @c NO

View File

@@ -24,12 +24,10 @@
@interface FeedGroup (Ext) @interface FeedGroup (Ext)
/// Enum type to distinguish different @c FeedGroup types: @c GROUP, @c FEED, @c SEPARATOR /// Enum type to distinguish different @c FeedGroup types: @c GROUP, @c FEED, @c SEPARATOR
typedef enum int16_t { typedef NS_ENUM(int16_t, FeedGroupType) {
/// Other types: @c GROUP, @c FEED, @c SEPARATOR /// Other types: @c GROUP, @c FEED, @c SEPARATOR
GROUP = 0, GROUP = 0, FEED = 1, SEPARATOR = 2
FEED = 1, };
SEPARATOR = 2
} FeedGroupType;
@property (readonly) FeedGroupType typ; @property (readonly) FeedGroupType typ;

View File

@@ -23,12 +23,18 @@
#import "FeedMeta+CoreDataClass.h" #import "FeedMeta+CoreDataClass.h"
@interface FeedMeta (Ext) @interface FeedMeta (Ext)
/// Easy memorable enum type for refresh unit index
typedef NS_ENUM(int16_t, RefreshUnitType) {
/// Other types: @c GROUP, @c FEED, @c SEPARATOR
RefreshUnitSeconds = 0, RefreshUnitMinutes = 1, RefreshUnitHours = 2, RefreshUnitDays = 3, RefreshUnitWeeks = 4
};
- (void)setErrorAndPostponeSchedule; - (void)setErrorAndPostponeSchedule;
- (void)calculateAndSetScheduled; - (void)calculateAndSetScheduled;
- (void)setEtag:(NSString*)etag modified:(NSString*)modified; - (void)setEtag:(NSString*)etag modified:(NSString*)modified;
- (void)setEtagAndModified:(NSHTTPURLResponse*)http; - (void)setEtagAndModified:(NSHTTPURLResponse*)http;
- (BOOL)setURL:(NSString*)url refresh:(int32_t)refresh unit:(int16_t)unit; - (BOOL)setURL:(NSString*)url refresh:(int32_t)refresh unit:(RefreshUnitType)unit;
- (NSString*)readableRefreshString; - (NSString*)readableRefreshString;
@end @end

View File

@@ -59,7 +59,7 @@
@return @c YES if refresh interval has changed @return @c YES if refresh interval has changed
*/ */
- (BOOL)setURL:(NSString*)url refresh:(int32_t)refresh unit:(int16_t)unit { - (BOOL)setURL:(NSString*)url refresh:(int32_t)refresh unit:(RefreshUnitType)unit {
BOOL intervalChanged = (self.refreshNum != refresh || self.refreshUnit != unit); BOOL intervalChanged = (self.refreshNum != refresh || self.refreshUnit != unit);
if (![self.url isEqualToString:url]) self.url = url; if (![self.url isEqualToString:url]) self.url = url;
if (self.refreshNum != refresh) self.refreshNum = refresh; if (self.refreshNum != refresh) self.refreshNum = refresh;

View File

@@ -28,7 +28,8 @@
+ (void)registerNetworkChangeNotification; + (void)registerNetworkChangeNotification;
+ (void)unregisterNetworkChangeNotification; + (void)unregisterNetworkChangeNotification;
// Scheduled feed update // Scheduled feed update
+ (void)newFeed:(NSString *)url block:(void(^)(RSParsedFeed *feed, NSError* error, NSHTTPURLResponse* response))block; + (void)newFeed:(NSString *)urlStr block:(void(^)(RSParsedFeed *feed, NSError *error, NSHTTPURLResponse *response))block;
+ (void)autoDownloadAndParseURL:(NSString*)url;
+ (void)scheduleUpdateForUpcomingFeeds; + (void)scheduleUpdateForUpcomingFeeds;
+ (void)forceUpdateAllFeeds; + (void)forceUpdateAllFeeds;
// User interaction // User interaction

View File

@@ -25,6 +25,7 @@
#import "StoreCoordinator.h" #import "StoreCoordinator.h"
#import "Feed+Ext.h" #import "Feed+Ext.h"
#import "FeedMeta+Ext.h" #import "FeedMeta+Ext.h"
#import "FeedGroup+Ext.h"
#import <SystemConfiguration/SystemConfiguration.h> #import <SystemConfiguration/SystemConfiguration.h>
@@ -151,10 +152,15 @@ static BOOL _nextUpdateIsForced = NO;
/// @return New request with no caching policy and timeout interval of 30 seconds. /// @return New request with no caching policy and timeout interval of 30 seconds.
+ (NSMutableURLRequest*)newRequestURL:(NSString*)url { + (NSMutableURLRequest*)newRequestURL:(NSString*)urlStr {
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:url]]; NSURL *url = [NSURL URLWithString:urlStr];
req.timeoutInterval = 30; if (!url.scheme) {
req.cachePolicy = NSURLRequestReloadIgnoringCacheData; url = [NSURL URLWithString:[NSString stringWithFormat:@"http://%@", urlStr]]; // usually will redirect to https if necessary
}
NSMutableURLRequest *req = [NSMutableURLRequest requestWithURL:url];
req.cachePolicy = NSURLRequestReloadIgnoringLocalCacheData;
req.HTTPShouldHandleCookies = NO;
// req.timeoutInterval = 30;
// [req setValue:@"Mon, 10 Sep 2018 10:32:19 GMT" forHTTPHeaderField:@"If-Modified-Since"]; // [req setValue:@"Mon, 10 Sep 2018 10:32:19 GMT" forHTTPHeaderField:@"If-Modified-Since"];
// [req setValue:@"wII2pETT9EGmlqyCHBFJpm25/7w" forHTTPHeaderField:@"If-None-Match"]; // ETag // [req setValue:@"wII2pETT9EGmlqyCHBFJpm25/7w" forHTTPHeaderField:@"If-None-Match"]; // ETag
return req; return req;
@@ -168,25 +174,25 @@ static BOOL _nextUpdateIsForced = NO;
[req setValue:meta.modified forHTTPHeaderField:@"If-Modified-Since"]; [req setValue:meta.modified forHTTPHeaderField:@"If-Modified-Since"];
if (etag.length > 0) if (etag.length > 0)
[req setValue:etag forHTTPHeaderField:@"If-None-Match"]; // ETag [req setValue:etag forHTTPHeaderField:@"If-None-Match"]; // ETag
if (!_nextUpdateIsForced) // any FeedMeta-request that is not forced, is a background update
req.networkServiceType = NSURLNetworkServiceTypeBackground;
return req; return req;
} }
/** /**
Perform feed download request from URL alone. Not updating any @c Feed item. Perform feed download request from URL alone. Not updating any @c Feed item.
*/ */
+ (void)newFeed:(NSString *)url block:(void(^)(RSParsedFeed *feed, NSError* error, NSHTTPURLResponse* response))block { + (void)newFeed:(NSString *)urlStr block:(void(^)(RSParsedFeed *feed, NSError *error, NSHTTPURLResponse *response))block {
[[[NSURLSession sharedSession] dataTaskWithRequest:[self newRequestURL:url] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { [[[NSURLSession sharedSession] dataTaskWithRequest:[self newRequestURL:urlStr] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response; NSHTTPURLResponse* httpResponse = (NSHTTPURLResponse*)response;
if (error || [httpResponse statusCode] == 304) { if (error || [httpResponse statusCode] == 304) {
block(nil, error, httpResponse); block(nil, error, httpResponse);
return; return;
} }
RSXMLData *xml = [[RSXMLData alloc] initWithData:data urlString:url]; RSXMLData *xml = [[RSXMLData alloc] initWithData:data urlString:urlStr];
RSParseFeed(xml, ^(RSParsedFeed * _Nullable parsedFeed, NSError * _Nullable err) { RSParseFeed(xml, ^(RSParsedFeed * _Nullable parsedFeed, NSError * _Nullable err) {
if (!err && (!parsedFeed || parsedFeed.articles.count == 0)) { // TODO: this should be fixed in RSXMLParser NSAssert(err || parsedFeed, @"Only parse error XOR parsed result can be set. Not both. Neither none.");
NSString *errDesc = NSLocalizedString(@"URL does not contain a RSS feed. Can't parse feed items.", nil); // TODO: Need for error?: "URL does not contain a RSS feed. Can't parse feed items."
err = [NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorUnsupportedURL userInfo:@{NSLocalizedDescriptionKey: errDesc}];
}
block(parsedFeed, err, httpResponse); block(parsedFeed, err, httpResponse);
}); });
}] resume]; }] resume];
@@ -203,26 +209,26 @@ static BOOL _nextUpdateIsForced = NO;
return; return;
dispatch_group_enter(group); dispatch_group_enter(group);
[[[NSURLSession sharedSession] dataTaskWithRequest:[self newRequest:feed.meta] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { [[[NSURLSession sharedSession] dataTaskWithRequest:[self newRequest:feed.meta] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) {
[feed.managedObjectContext performBlock:^{ NSHTTPURLResponse *header = (NSHTTPURLResponse*)response;
// core data block inside of url session block; otherwise access will EXC_BAD_INSTRUCTION RSParsedFeed *parsed = nil; // can stay nil if !error and statusCode = 304
if (error) { BOOL hasError = (error != nil);
if (!error && [header statusCode] != 304) { // only parse if modified
RSXMLData *xml = [[RSXMLData alloc] initWithData:data urlString:header.URL.absoluteString];
// should be fine to call synchronous since dataTask is already in the background (always? proof?)
parsed = RSParseFeedSync(xml, &error); // reuse error
if (error || !parsed || parsed.articles.count == 0) {
hasError = YES;
}
}
[feed.managedObjectContext performBlock:^{ // otherwise access on feed will EXC_BAD_INSTRUCTION
if (hasError) {
[feed.meta setErrorAndPostponeSchedule]; [feed.meta setErrorAndPostponeSchedule];
} else { } else {
[feed.meta setEtagAndModified:(NSHTTPURLResponse*)response]; feed.meta.errorCount = 0; // reset counter
[feed.meta setEtagAndModified:header];
[feed.meta calculateAndSetScheduled]; [feed.meta calculateAndSetScheduled];
if (parsed) [feed updateWithRSS:parsed postUnreadCountChange:YES];
if ([(NSHTTPURLResponse*)response statusCode] != 304) { // only parse if modified // TODO: save changes for this feed only? / Partial Update
// should be fine to call synchronous since dataTask is already in the background (always? proof?)
RSXMLData *xml = [[RSXMLData alloc] initWithData:data urlString:feed.meta.url];
RSParsedFeed *parsed = RSParseFeedSync(xml, NULL);
if (parsed && parsed.articles.count > 0) {
[feed updateWithRSS:parsed postUnreadCountChange:YES];
feed.meta.errorCount = 0; // reset counter
} else {
[feed.meta setErrorAndPostponeSchedule]; // replaces date of 'calculateAndSetScheduled'
}
}
// TODO: save changes for this feed only?
//[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:feed.objectID]; //[[NSNotificationCenter defaultCenter] postNotificationName:kNotificationFeedUpdated object:feed.objectID];
} }
dispatch_group_leave(group); dispatch_group_leave(group);
@@ -230,6 +236,47 @@ static BOOL _nextUpdateIsForced = NO;
}] resume]; }] resume];
} }
/**
Download feed at url and append to persistent store in root folder.
On error present user modal alert.
*/
+ (void)autoDownloadAndParseURL:(NSString*)url {
[FeedDownload newFeed:url block:^(RSParsedFeed *feed, NSError *error, NSHTTPURLResponse *response) {
if (error) {
dispatch_async(dispatch_get_main_queue(), ^{
[NSApp presentError:error];
});
} else {
[FeedDownload autoParseFeedAndAppendToRoot:feed response:response];
}
}];
}
/**
Create new @c FeedGroup, @c Feed, @c FeedMeta and @c FeedArticle instances and save them to the persistent store.
Appends feed to the end of the root folder, so that the user will immediatelly see it.
Update duration is set to the default of 30 minutes.
@param rss Parsed RSS feed. If @c @c nil no feed object will be added.
@param response May be @c nil but then feed download URL will not be set.
*/
+ (void)autoParseFeedAndAppendToRoot:(nonnull RSParsedFeed*)rss response:(NSHTTPURLResponse*)response {
if (!rss || rss.articles.count == 0) return;
NSManagedObjectContext *moc = [StoreCoordinator createChildContext];
NSUInteger idx = [StoreCoordinator sortedObjectIDsForParent:nil isFeed:NO inContext:moc].count;
FeedGroup *newFeed = [FeedGroup newGroup:FEED inContext:moc];
FeedMeta *meta = newFeed.feed.meta;
[meta setURL:response.URL.absoluteString refresh:30 unit:RefreshUnitMinutes];
[meta calculateAndSetScheduled];
[newFeed setName:rss.title andRefreshString:[meta readableRefreshString]];
[meta setEtagAndModified:response];
[newFeed.feed updateWithRSS:rss postUnreadCountChange:YES];
newFeed.sortIndex = (int32_t)idx;
[newFeed.feed calculateAndSetIndexPathString];
[StoreCoordinator saveContext:moc andParent:YES];
[moc reset];
}
#pragma mark - Network Connection & Reachability - #pragma mark - Network Connection & Reachability -

View File

@@ -60,12 +60,10 @@
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChanged:) name:kNotificationNetworkStatusChanged object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(networkChanged:) name:kNotificationNetworkStatusChanged object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(unreadCountChanged:) name:kNotificationTotalUnreadCountChanged object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(unreadCountChanged:) name:kNotificationTotalUnreadCountChanged object:nil];
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(asyncReloadUnreadCountAndUpdateBarIcon) name:kNotificationTotalUnreadCountReset object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(asyncReloadUnreadCountAndUpdateBarIcon) name:kNotificationTotalUnreadCountReset object:nil];
[FeedDownload registerNetworkChangeNotification]; // will call update scheduler
return self; return self;
} }
- (void)dealloc { - (void)dealloc {
[FeedDownload unregisterNetworkChangeNotification];
[[NSNotificationCenter defaultCenter] removeObserver:self]; [[NSNotificationCenter defaultCenter] removeObserver:self];
} }